rulesync 16.9.1 → 16.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/cli/index.cjs +5 -5
- package/dist/cli/index.js +5 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-BpKoN2US.js → import-CXJwVed1.js} +1750 -127
- package/dist/import-CXJwVed1.js.map +1 -0
- package/dist/{import-BaRDaZET.cjs → import-danhPI2x.cjs} +1764 -141
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-BpKoN2US.js.map +0 -1
|
@@ -134,27 +134,27 @@ const isFeatureValueEnabled = (value) => {
|
|
|
134
134
|
};
|
|
135
135
|
//#endregion
|
|
136
136
|
//#region src/constants/rulesync-paths.ts
|
|
137
|
-
const { join: join$
|
|
137
|
+
const { join: join$288 } = node_path.posix;
|
|
138
138
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
139
139
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
140
140
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
141
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
142
|
-
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$
|
|
143
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
144
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
145
|
-
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$
|
|
146
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
147
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
148
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
149
|
-
join$
|
|
150
|
-
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
151
|
-
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
141
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "rules");
|
|
142
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$288(RULESYNC_RULES_RELATIVE_DIR_PATH, ".curated");
|
|
143
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "commands");
|
|
144
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "subagents");
|
|
145
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "checks");
|
|
146
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
147
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
148
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
149
|
+
join$288(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
150
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
151
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
152
152
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
153
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
153
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
154
154
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
155
155
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
156
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
157
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
156
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$288(RULESYNC_RELATIVE_DIR_PATH, "skills");
|
|
157
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$288(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
|
|
158
158
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
159
159
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
160
160
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -196,6 +196,7 @@ const rulesProcessorToolTargetTuple = [
|
|
|
196
196
|
"kiro",
|
|
197
197
|
"kiro-cli",
|
|
198
198
|
"kiro-ide",
|
|
199
|
+
"musecode",
|
|
199
200
|
"opencode",
|
|
200
201
|
"pi",
|
|
201
202
|
"qwencode",
|
|
@@ -259,6 +260,7 @@ const mcpProcessorToolTargetTuple = [
|
|
|
259
260
|
"kiro-cli",
|
|
260
261
|
"kiro-ide",
|
|
261
262
|
"junie",
|
|
263
|
+
"musecode",
|
|
262
264
|
"opencode",
|
|
263
265
|
"qwencode",
|
|
264
266
|
"reasonix",
|
|
@@ -366,6 +368,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
366
368
|
"kiro",
|
|
367
369
|
"kiro-cli",
|
|
368
370
|
"kiro-ide",
|
|
371
|
+
"musecode",
|
|
369
372
|
"opencode",
|
|
370
373
|
"pi",
|
|
371
374
|
"qwencode",
|
|
@@ -420,6 +423,7 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
420
423
|
"cline",
|
|
421
424
|
"codexcli",
|
|
422
425
|
"copilot",
|
|
426
|
+
"copilotcli",
|
|
423
427
|
"cursor",
|
|
424
428
|
"devin",
|
|
425
429
|
"factorydroid",
|
|
@@ -443,6 +447,7 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
443
447
|
];
|
|
444
448
|
const checksProcessorToolTargetTuple = [
|
|
445
449
|
"amp",
|
|
450
|
+
"augmentcode",
|
|
446
451
|
"cursor",
|
|
447
452
|
"hermesagent",
|
|
448
453
|
"rovodev",
|
|
@@ -1755,6 +1760,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
|
|
|
1755
1760
|
".claude/settings.json",
|
|
1756
1761
|
".claude/settings.local.json",
|
|
1757
1762
|
".codex/config.toml",
|
|
1763
|
+
".copilot/settings.json",
|
|
1764
|
+
".github/copilot/settings.json",
|
|
1758
1765
|
".devin/config.json",
|
|
1759
1766
|
".factory/settings.json",
|
|
1760
1767
|
".grok/config.toml",
|
|
@@ -2613,8 +2620,8 @@ const CODEXCLI_HOOK_EVENTS = [
|
|
|
2613
2620
|
* has NO `SubagentStart`/`SubagentStop` arms — emitting them would write keys
|
|
2614
2621
|
* Goose silently ignores, so `subagentStart`/`subagentStop` are intentionally
|
|
2615
2622
|
* excluded here and from `CANONICAL_TO_GOOSE_EVENT_NAMES`.
|
|
2616
|
-
* @see https://github.com/
|
|
2617
|
-
* @see https://
|
|
2623
|
+
* @see https://github.com/aaif-goose/goose/blob/v1.41.0/crates/goose/src/hooks/mod.rs
|
|
2624
|
+
* @see https://goose-docs.ai/docs/guides/context-engineering/hooks/
|
|
2618
2625
|
*/
|
|
2619
2626
|
const GOOSE_HOOK_EVENTS = [
|
|
2620
2627
|
"sessionStart",
|
|
@@ -2651,8 +2658,8 @@ const KIRO_HOOK_EVENTS = [
|
|
|
2651
2658
|
* `UserPromptSubmit`, `PreToolUse`, and `PostToolUse`. Kiro also documents
|
|
2652
2659
|
* file-event (`PostFileCreate`/`PostFileSave`/`PostFileDelete`) and spec-task
|
|
2653
2660
|
* (`PreTaskExec`/`PostTaskExec`) triggers that have no canonical equivalent;
|
|
2654
|
-
* those can still be emitted verbatim via
|
|
2655
|
-
*
|
|
2661
|
+
* those can still be emitted verbatim via the shared `kiro` override block
|
|
2662
|
+
* (unknown event keys pass through unchanged). There is no `SessionEnd`
|
|
2656
2663
|
* trigger, so the canonical `sessionEnd` has no home here.
|
|
2657
2664
|
* @see https://kiro.dev/docs/hooks/types/
|
|
2658
2665
|
*/
|
|
@@ -3385,6 +3392,47 @@ const CANONICAL_TO_KIRO_EVENT_NAMES = {
|
|
|
3385
3392
|
stop: "stop"
|
|
3386
3393
|
};
|
|
3387
3394
|
/**
|
|
3395
|
+
* Native event keys of the embedded agent-config hook format, as listed by the
|
|
3396
|
+
* Kiro CLI 3.0 migration guide's old-format column: `agentSpawn`,
|
|
3397
|
+
* `userPromptSubmit`, `preToolUse`, `postToolUse`, `fileEdited`, `fileCreated`
|
|
3398
|
+
* and `agentStop`/`stop`. Two of them (`fileEdited`, `fileCreated`) have no
|
|
3399
|
+
* canonical equivalent, so they exist only here.
|
|
3400
|
+
*
|
|
3401
|
+
* The `kiro` override block is shared with the standalone-format targets, whose
|
|
3402
|
+
* trigger vocabulary is different, so the alias writer uses this list to decide
|
|
3403
|
+
* what it can express.
|
|
3404
|
+
* @see https://kiro.dev/docs/cli/v3/hooks-migration/
|
|
3405
|
+
*/
|
|
3406
|
+
const KIRO_AGENT_CONFIG_NATIVE_EVENT_NAMES = [
|
|
3407
|
+
"agentSpawn",
|
|
3408
|
+
"userPromptSubmit",
|
|
3409
|
+
"preToolUse",
|
|
3410
|
+
"postToolUse",
|
|
3411
|
+
"fileEdited",
|
|
3412
|
+
"fileCreated",
|
|
3413
|
+
"agentStop",
|
|
3414
|
+
"stop"
|
|
3415
|
+
];
|
|
3416
|
+
/**
|
|
3417
|
+
* Old agent-config event keys mapped to their standalone v1 trigger, so a
|
|
3418
|
+
* `kiro.hooks` block authored in the deprecated spelling still emits a valid
|
|
3419
|
+
* trigger for the `kiro-cli` / `kiro-ide` targets that read the same block.
|
|
3420
|
+
* The migration guide states the old names "map directly to their newer
|
|
3421
|
+
* equivalents" and spells out `agentSpawn` → `SessionStart` and `fileEdited` →
|
|
3422
|
+
* `PostFileSave`; the rest are the same event under the v1 casing.
|
|
3423
|
+
* @see https://kiro.dev/docs/cli/v3/hooks-migration/
|
|
3424
|
+
*/
|
|
3425
|
+
const KIRO_LEGACY_TO_KIRO_IDE_TRIGGER_NAMES = {
|
|
3426
|
+
agentSpawn: "SessionStart",
|
|
3427
|
+
userPromptSubmit: "UserPromptSubmit",
|
|
3428
|
+
preToolUse: "PreToolUse",
|
|
3429
|
+
postToolUse: "PostToolUse",
|
|
3430
|
+
fileEdited: "PostFileSave",
|
|
3431
|
+
fileCreated: "PostFileCreate",
|
|
3432
|
+
agentStop: "Stop",
|
|
3433
|
+
stop: "Stop"
|
|
3434
|
+
};
|
|
3435
|
+
/**
|
|
3388
3436
|
* Map Kiro CLI camelCase event names to canonical camelCase.
|
|
3389
3437
|
*/
|
|
3390
3438
|
const KIRO_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_KIRO_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
@@ -3392,8 +3440,8 @@ const KIRO_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICA
|
|
|
3392
3440
|
* Map canonical camelCase event names to Kiro IDE PascalCase triggers.
|
|
3393
3441
|
*
|
|
3394
3442
|
* Only the canonical lifecycle events with a clean IDE equivalent are mapped.
|
|
3395
|
-
* Unknown keys (e.g. IDE-only `PostFileSave`/`PreTaskExec` set via
|
|
3396
|
-
* override) pass through unchanged.
|
|
3443
|
+
* Unknown keys (e.g. IDE-only `PostFileSave`/`PreTaskExec` set via the shared
|
|
3444
|
+
* `kiro` override block) pass through unchanged.
|
|
3397
3445
|
* @see https://kiro.dev/docs/hooks/types/
|
|
3398
3446
|
*/
|
|
3399
3447
|
const CANONICAL_TO_KIRO_IDE_EVENT_NAMES = {
|
|
@@ -4894,13 +4942,57 @@ const CodexcliPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
4894
4942
|
git_write_rules: zod_mini.z.optional(zod_mini.z.boolean())
|
|
4895
4943
|
});
|
|
4896
4944
|
/**
|
|
4945
|
+
* Tool-scoped override block for Zed. Two Zed surfaces sit outside the canonical
|
|
4946
|
+
* allow/ask/deny model that `agent.tool_permissions` implements, and both are
|
|
4947
|
+
* authored here verbatim:
|
|
4948
|
+
*
|
|
4949
|
+
* - `sandbox_permissions` — the OS-level agent sandbox, on by default since Zed
|
|
4950
|
+
* 1.14.2 for the `terminal` and `fetch` tools. Its defaults forbid network
|
|
4951
|
+
* access, writing outside the project directories, and writing to `.git`, so a
|
|
4952
|
+
* real setup usually needs to relax one of them (`network_hosts` with exact
|
|
4953
|
+
* hostnames or leading `*.` wildcards, `allow_all_hosts`, `write_paths`,
|
|
4954
|
+
* `allow_fs_write_all`, `allow_unsandboxed`). This is containment, not tool
|
|
4955
|
+
* gating: no canonical category expresses it, so the block is written straight
|
|
4956
|
+
* into `agent.sandbox_permissions`, mirroring the Kilo / Claude Code / Codex
|
|
4957
|
+
* CLI `sandbox` passthrough precedent. Kept a bare `looseObject` rather than
|
|
4958
|
+
* enumerating the keys, since upstream adds to them release over release.
|
|
4959
|
+
* - `profiles` — the tool-availability layer, a separate enforcement stage from
|
|
4960
|
+
* `tool_permissions`: a tool absent from the active profile cannot be used no
|
|
4961
|
+
* matter what the permission rules say. Per-profile keys are `name`, `tools`
|
|
4962
|
+
* (per-tool booleans), `enable_all_context_servers`, `context_servers` and
|
|
4963
|
+
* `default_model`. Like Kimi Code's `tools.enabled`/`disabled` block this is a
|
|
4964
|
+
* verbatim passthrough — no canonicalization is attempted.
|
|
4965
|
+
*
|
|
4966
|
+
* Neither surface can weaken a canonical deny, because `agent.tool_permissions`
|
|
4967
|
+
* is off-limits to this override: the translator consumes only the two keys
|
|
4968
|
+
* above by name and rejects a `tool_permissions` key with a warning.
|
|
4969
|
+
*
|
|
4970
|
+
* @see https://zed.dev/docs/ai/sandboxing
|
|
4971
|
+
* @see https://zed.dev/docs/ai/agent-profiles
|
|
4972
|
+
*
|
|
4973
|
+
* @example
|
|
4974
|
+
* { "sandbox_permissions": { "network_hosts": ["*.github.com"], "write_paths": ["/tmp"] },
|
|
4975
|
+
* "profiles": { "review": { "name": "Review", "tools": { "terminal": false } } } }
|
|
4976
|
+
*/
|
|
4977
|
+
const ZedPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
4978
|
+
permission: zod_mini.z.optional(ToolScopedPermissionSchema),
|
|
4979
|
+
sandbox_permissions: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
4980
|
+
profiles: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.looseObject({
|
|
4981
|
+
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
4982
|
+
tools: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.boolean())),
|
|
4983
|
+
enable_all_context_servers: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
4984
|
+
context_servers: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
4985
|
+
default_model: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
4986
|
+
})))
|
|
4987
|
+
});
|
|
4988
|
+
/**
|
|
4897
4989
|
* Permissions configuration.
|
|
4898
4990
|
* Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
|
|
4899
4991
|
* Values are pattern-to-action mappings for that tool category.
|
|
4900
4992
|
*
|
|
4901
4993
|
* The optional `opencode`/`hermes`/`cline`/`kilo`/`claudecode`/`vibe`/`cursor`/
|
|
4902
4994
|
* `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie`/`takt`/`amp`/
|
|
4903
|
-
* `antigravity-cli`/`augmentcode`/`kiro`/`codexcli` keys are tool-scoped
|
|
4995
|
+
* `antigravity-cli`/`augmentcode`/`kiro`/`codexcli`/`zed` keys are tool-scoped
|
|
4904
4996
|
* overrides consumed only by their respective translator (see the matching
|
|
4905
4997
|
* `*PermissionsOverrideSchema`); every other tool reads the shared `permission`
|
|
4906
4998
|
* block and ignores them.
|
|
@@ -4940,14 +5032,15 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
|
|
|
4940
5032
|
augmentcode: zod_mini.z.optional(AugmentcodePermissionsOverrideSchema),
|
|
4941
5033
|
kiro: zod_mini.z.optional(KiroPermissionsOverrideSchema),
|
|
4942
5034
|
codexcli: zod_mini.z.optional(CodexcliPermissionsOverrideSchema),
|
|
5035
|
+
zed: zod_mini.z.optional(ZedPermissionsOverrideSchema),
|
|
4943
5036
|
"antigravity-ide": zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
4944
5037
|
copilot: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
5038
|
+
copilotcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
4945
5039
|
devin: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
4946
5040
|
goose: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
4947
5041
|
grokcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
4948
5042
|
"kimi-code": zod_mini.z.optional(KimiCodePermissionsOverrideSchema),
|
|
4949
|
-
rovodev: zod_mini.z.optional(CanonicalPermissionsOverrideSchema)
|
|
4950
|
-
zed: zod_mini.z.optional(CanonicalPermissionsOverrideSchema)
|
|
5043
|
+
rovodev: zod_mini.z.optional(CanonicalPermissionsOverrideSchema)
|
|
4951
5044
|
});
|
|
4952
5045
|
/**
|
|
4953
5046
|
* Full permissions file schema including optional $schema field.
|
|
@@ -5660,6 +5753,33 @@ function resolveUserInvocable({ rootFrontmatter, section }) {
|
|
|
5660
5753
|
return section?.["user-invocable"] ?? rootFrontmatter["user-invocable"];
|
|
5661
5754
|
}
|
|
5662
5755
|
//#endregion
|
|
5756
|
+
//#region src/constants/augmentcode-paths.ts
|
|
5757
|
+
const AUGMENTCODE_DIR = ".augment";
|
|
5758
|
+
const AUGMENTCODE_COMMANDS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "commands");
|
|
5759
|
+
/**
|
|
5760
|
+
* The cross-tool `.agents/commands/` root Auggie also discovers commands from.
|
|
5761
|
+
* Auggie resolves commands over `.augment`, `.claude` and `.agents` in both the
|
|
5762
|
+
* workspace and the home directory; rulesync writes to its own `.augment` root
|
|
5763
|
+
* and reads this one as well, mirroring how skills treat `.agents/skills`.
|
|
5764
|
+
* @see https://docs.augmentcode.com/cli/custom-commands
|
|
5765
|
+
*/
|
|
5766
|
+
const AUGMENTCODE_AGENTS_COMMANDS_DIR_PATH = (0, node_path.join)(".agents", "commands");
|
|
5767
|
+
const AUGMENTCODE_SKILLS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "skills");
|
|
5768
|
+
const AUGMENTCODE_AGENTS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "agents");
|
|
5769
|
+
const AUGMENTCODE_ALT_AGENTS_DIR_PATH = ".agents";
|
|
5770
|
+
const AUGMENTCODE_AGENTS_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
|
|
5771
|
+
const AUGMENTCODE_SETTINGS_FILE_NAME = "settings.json";
|
|
5772
|
+
const AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
|
|
5773
|
+
/**
|
|
5774
|
+
* Augment Code Review's custom guidelines file. Read from the repository root's
|
|
5775
|
+
* `.augment/` folder, project scope only — Augment documents no user-level
|
|
5776
|
+
* equivalent, and the reviewer runs against the committed tree.
|
|
5777
|
+
* @see https://docs.augmentcode.com/codereview/review-guidelines
|
|
5778
|
+
*/
|
|
5779
|
+
const AUGMENTCODE_CODE_REVIEW_GUIDELINES_FILE_NAME = "code_review_guidelines.yaml";
|
|
5780
|
+
const AUGMENTCODE_IGNORE_FILE_NAME = ".augmentignore";
|
|
5781
|
+
const AUGMENTCODE_LEGACY_RULE_FILE_NAME = ".augment-guidelines";
|
|
5782
|
+
//#endregion
|
|
5663
5783
|
//#region src/constants/cursor-paths.ts
|
|
5664
5784
|
const CURSOR_DIR = ".cursor";
|
|
5665
5785
|
const CURSOR_COMMANDS_DIR_PATH = (0, node_path.join)(CURSOR_DIR, "commands");
|
|
@@ -5709,6 +5829,19 @@ const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
|
|
|
5709
5829
|
*/
|
|
5710
5830
|
const TAKT_CONFIG_FILE_NAME = "config.yaml";
|
|
5711
5831
|
/**
|
|
5832
|
+
* Takt's runtime provider config (Takt 0.56.0+). Lives at `.takt/runtime.yaml`
|
|
5833
|
+
* (project) and `~/.takt/runtime.yaml` (global), and owns provider/model/
|
|
5834
|
+
* provider-option assignment. Takt generates the global file on first launch,
|
|
5835
|
+
* so most installs have one; "runtime mode" only activates when its `provider:`
|
|
5836
|
+
* section carries an actual assignment.
|
|
5837
|
+
*
|
|
5838
|
+
* rulesync only ever READS this file: it resolves the active provider from it,
|
|
5839
|
+
* and it refuses to write the legacy `provider_options` key into `config.yaml`
|
|
5840
|
+
* while runtime mode is active (Takt hard-fails on that combination).
|
|
5841
|
+
* @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
|
|
5842
|
+
*/
|
|
5843
|
+
const TAKT_RUNTIME_CONFIG_FILE_NAME = "runtime.yaml";
|
|
5844
|
+
/**
|
|
5712
5845
|
* Top-level key in Takt's `config.yaml` holding the workflow MCP transport
|
|
5713
5846
|
* allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
|
|
5714
5847
|
* transport must be explicitly enabled here before any workflow-defined MCP
|
|
@@ -6184,6 +6317,363 @@ function slugifyCheckName(value) {
|
|
|
6184
6317
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/, "");
|
|
6185
6318
|
}
|
|
6186
6319
|
//#endregion
|
|
6320
|
+
//#region src/features/checks/augmentcode-check.ts
|
|
6321
|
+
/** Augment's severity scale. Narrower than the canonical one. */
|
|
6322
|
+
const AUGMENTCODE_SEVERITIES = [
|
|
6323
|
+
"high",
|
|
6324
|
+
"medium",
|
|
6325
|
+
"low"
|
|
6326
|
+
];
|
|
6327
|
+
/**
|
|
6328
|
+
* Canonical severity to Augment's scale. Augment has no band above `high`, so
|
|
6329
|
+
* canonical `critical` folds into it — the alternative, dropping the rule or
|
|
6330
|
+
* demoting it to `medium`, would either lose the check or understate the one
|
|
6331
|
+
* severity a reviewer most wants raised. The fold is one-way: a `critical`
|
|
6332
|
+
* check generates `high` and imports back as `high`, so the canonical value is
|
|
6333
|
+
* not recoverable from Augment's file alone.
|
|
6334
|
+
*/
|
|
6335
|
+
const CANONICAL_TO_AUGMENTCODE_SEVERITY = {
|
|
6336
|
+
low: "low",
|
|
6337
|
+
medium: "medium",
|
|
6338
|
+
high: "high",
|
|
6339
|
+
critical: "high"
|
|
6340
|
+
};
|
|
6341
|
+
/**
|
|
6342
|
+
* Every field of an `areas.<key>` entry is required upstream, so a check with no
|
|
6343
|
+
* `severity` still has to emit one. `medium` is the neutral middle of Augment's
|
|
6344
|
+
* three bands: defaulting to `high` would inflate every unannotated check past
|
|
6345
|
+
* the ones deliberately marked `medium`, and `low` would bury them.
|
|
6346
|
+
*/
|
|
6347
|
+
const DEFAULT_AUGMENTCODE_SEVERITY = "medium";
|
|
6348
|
+
/**
|
|
6349
|
+
* Augment's own example uses `["**"]`, and `globs` is required per area. Built
|
|
6350
|
+
* fresh per area rather than shared: js-yaml serializes a repeated reference as
|
|
6351
|
+
* an anchor/alias pair, which would litter a file Augment tells users to edit by
|
|
6352
|
+
* hand with `&ref_0` / `*ref_0`.
|
|
6353
|
+
*/
|
|
6354
|
+
const defaultAreaGlobs = () => ["**"];
|
|
6355
|
+
/**
|
|
6356
|
+
* The `augmentcode` block of a check's frontmatter, carrying the two things the
|
|
6357
|
+
* canonical check model has no home for: which area a rule belongs to (so
|
|
6358
|
+
* several checks can share one), and the area's `globs`.
|
|
6359
|
+
*/
|
|
6360
|
+
const AugmentcodeCheckOverrideSchema = zod_mini.z.looseObject({
|
|
6361
|
+
/** Area key this check's rule is grouped under; defaults to the check's slug. */
|
|
6362
|
+
area: zod_mini.z.optional(zod_mini.z.string().check(zod_mini.z.minLength(1))),
|
|
6363
|
+
/** Human-readable `areas.<key>.description`; defaults to the check's own. */
|
|
6364
|
+
areaDescription: zod_mini.z.optional(zod_mini.z.string()),
|
|
6365
|
+
globs: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string().check(zod_mini.z.minLength(1)))),
|
|
6366
|
+
/** Rule `id`; defaults to the check's slug. */
|
|
6367
|
+
id: zod_mini.z.optional(zod_mini.z.string().check(zod_mini.z.minLength(1)))
|
|
6368
|
+
});
|
|
6369
|
+
function parseOverride$1(raw, filePath, logger) {
|
|
6370
|
+
if (raw === void 0) return {};
|
|
6371
|
+
if (!isPlainObject$1(raw)) {
|
|
6372
|
+
logger?.warn(`Ignoring the \`augmentcode\` block in ${filePath}: expected a mapping.`);
|
|
6373
|
+
return {};
|
|
6374
|
+
}
|
|
6375
|
+
const result = AugmentcodeCheckOverrideSchema.safeParse(raw);
|
|
6376
|
+
if (!result.success) throw new Error(`Invalid \`augmentcode\` block in ${filePath}: ${result.error.message}`, { cause: result.error });
|
|
6377
|
+
return result.data;
|
|
6378
|
+
}
|
|
6379
|
+
function stemOf$1(rulesyncCheck) {
|
|
6380
|
+
return (0, node_path.basename)(rulesyncCheck.getRelativeFilePath(), ".md");
|
|
6381
|
+
}
|
|
6382
|
+
/**
|
|
6383
|
+
* Disambiguate a name against the ones already taken. Used for rule ids on
|
|
6384
|
+
* generate (two same-named checks in different subdirectories) and for check
|
|
6385
|
+
* file names on import (one rule id repeated across two areas), since a
|
|
6386
|
+
* collision in either direction silently loses one of the pair.
|
|
6387
|
+
*/
|
|
6388
|
+
function uniqueName(preferred, used) {
|
|
6389
|
+
let name = preferred;
|
|
6390
|
+
let suffix = 2;
|
|
6391
|
+
while (used.has(name)) {
|
|
6392
|
+
name = `${preferred}-${suffix}`;
|
|
6393
|
+
suffix += 1;
|
|
6394
|
+
}
|
|
6395
|
+
used.add(name);
|
|
6396
|
+
return name;
|
|
6397
|
+
}
|
|
6398
|
+
/**
|
|
6399
|
+
* A rule's `description` is the whole instruction Augment acts on, so the check
|
|
6400
|
+
* body is preferred; the frontmatter `description` is a summary, used only when
|
|
6401
|
+
* there is no body. Both empty falls back to the check name, which at least
|
|
6402
|
+
* names the concern rather than emitting an empty required field.
|
|
6403
|
+
*/
|
|
6404
|
+
function toRuleDescription(rulesyncCheck) {
|
|
6405
|
+
const body = rulesyncCheck.getBody().trim();
|
|
6406
|
+
if (body.length > 0) return body;
|
|
6407
|
+
return rulesyncCheck.getFrontmatter().description?.trim() || stemOf$1(rulesyncCheck);
|
|
6408
|
+
}
|
|
6409
|
+
function toRule(rulesyncCheck, override, usedRuleIds) {
|
|
6410
|
+
const severity = rulesyncCheck.getFrontmatter().severity;
|
|
6411
|
+
return {
|
|
6412
|
+
id: uniqueName(override.id ?? slugifyCheckName(stemOf$1(rulesyncCheck)), usedRuleIds),
|
|
6413
|
+
description: toRuleDescription(rulesyncCheck),
|
|
6414
|
+
severity: severity ? CANONICAL_TO_AUGMENTCODE_SEVERITY[severity] ?? DEFAULT_AUGMENTCODE_SEVERITY : DEFAULT_AUGMENTCODE_SEVERITY
|
|
6415
|
+
};
|
|
6416
|
+
}
|
|
6417
|
+
/**
|
|
6418
|
+
* An authored area key is used verbatim. Slugifying it would rewrite the
|
|
6419
|
+
* underscores in Augment's own documented example (`memory_safety`), and since
|
|
6420
|
+
* import writes the key back unchanged, the next generate would build a second
|
|
6421
|
+
* area under the slugified spelling while the original stayed put — the same
|
|
6422
|
+
* rules twice. Only the file-stem default is slugified, because that one has to
|
|
6423
|
+
* become a legal area key from an arbitrary file name.
|
|
6424
|
+
*/
|
|
6425
|
+
function areaKeyFor(rulesyncCheck, override) {
|
|
6426
|
+
return override.area ?? slugifyCheckName(stemOf$1(rulesyncCheck));
|
|
6427
|
+
}
|
|
6428
|
+
/** Rule ids belonging to areas this generate preserves rather than rewrites. */
|
|
6429
|
+
function collectRuleIds(existingAreas, claimedKeys) {
|
|
6430
|
+
const ids = /* @__PURE__ */ new Set();
|
|
6431
|
+
for (const [areaKey, rawArea] of Object.entries(existingAreas)) {
|
|
6432
|
+
if (claimedKeys.has(areaKey) || !isPlainObject$1(rawArea)) continue;
|
|
6433
|
+
for (const rawRule of readArea(rawArea).rules) {
|
|
6434
|
+
const rule = readRule(rawRule);
|
|
6435
|
+
if (rule) ids.add(rule.id);
|
|
6436
|
+
}
|
|
6437
|
+
}
|
|
6438
|
+
return ids;
|
|
6439
|
+
}
|
|
6440
|
+
/**
|
|
6441
|
+
* Group the checks into `areas`. One area per check by default; checks naming
|
|
6442
|
+
* the same `augmentcode.area` share one, with the first check to name it
|
|
6443
|
+
* supplying the area's `description` and `globs`.
|
|
6444
|
+
*/
|
|
6445
|
+
function buildAreas({ entries, reservedRuleIds }) {
|
|
6446
|
+
const areas = /* @__PURE__ */ new Map();
|
|
6447
|
+
const usedRuleIds = new Set(reservedRuleIds);
|
|
6448
|
+
for (const { rulesyncCheck, override } of entries) {
|
|
6449
|
+
const key = areaKeyFor(rulesyncCheck, override);
|
|
6450
|
+
const rule = toRule(rulesyncCheck, override, usedRuleIds);
|
|
6451
|
+
const existing = areas.get(key);
|
|
6452
|
+
if (existing) {
|
|
6453
|
+
existing.rules.push(rule);
|
|
6454
|
+
continue;
|
|
6455
|
+
}
|
|
6456
|
+
areas.set(key, {
|
|
6457
|
+
description: override.areaDescription || rulesyncCheck.getFrontmatter().description?.trim() || stemOf$1(rulesyncCheck),
|
|
6458
|
+
globs: override.globs ?? defaultAreaGlobs(),
|
|
6459
|
+
rules: [rule]
|
|
6460
|
+
});
|
|
6461
|
+
}
|
|
6462
|
+
return Object.fromEntries(areas);
|
|
6463
|
+
}
|
|
6464
|
+
function parseGuidelines(fileContent, filePath) {
|
|
6465
|
+
if (fileContent.trim().length === 0) return {};
|
|
6466
|
+
let parsed;
|
|
6467
|
+
try {
|
|
6468
|
+
parsed = loadYaml(fileContent);
|
|
6469
|
+
} catch (error) {
|
|
6470
|
+
throw new Error(`Failed to parse AugmentCode code review guidelines at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
6471
|
+
}
|
|
6472
|
+
if (parsed === void 0 || parsed === null) return {};
|
|
6473
|
+
if (!isPlainObject$1(parsed)) throw new Error(`Failed to parse AugmentCode code review guidelines at ${filePath}: expected a mapping at the document root.`);
|
|
6474
|
+
return parsed;
|
|
6475
|
+
}
|
|
6476
|
+
/** An area's shared fields, read defensively out of hand-written YAML. */
|
|
6477
|
+
function readArea(rawArea) {
|
|
6478
|
+
const rawGlobs = Array.isArray(rawArea.globs) ? rawArea.globs : void 0;
|
|
6479
|
+
const globs = rawGlobs?.filter((glob) => typeof glob === "string");
|
|
6480
|
+
const authoredGlobs = globs && (globs.length > 0 || rawGlobs?.length === 0) ? globs : void 0;
|
|
6481
|
+
return {
|
|
6482
|
+
...typeof rawArea.description === "string" && { description: rawArea.description },
|
|
6483
|
+
...authoredGlobs && { globs: authoredGlobs },
|
|
6484
|
+
rules: Array.isArray(rawArea.rules) ? rawArea.rules : []
|
|
6485
|
+
};
|
|
6486
|
+
}
|
|
6487
|
+
/**
|
|
6488
|
+
* A rule missing `id` or `description` is not a shape Augment itself reads, so
|
|
6489
|
+
* it is skipped rather than imported as a check with an invented field.
|
|
6490
|
+
*/
|
|
6491
|
+
function readRule(rawRule) {
|
|
6492
|
+
if (!isPlainObject$1(rawRule)) return void 0;
|
|
6493
|
+
const id = typeof rawRule.id === "string" ? rawRule.id : void 0;
|
|
6494
|
+
const description = typeof rawRule.description === "string" ? rawRule.description : void 0;
|
|
6495
|
+
if (!id || !description) return void 0;
|
|
6496
|
+
const severity = AUGMENTCODE_SEVERITIES.find((value) => value === rawRule.severity);
|
|
6497
|
+
return {
|
|
6498
|
+
id,
|
|
6499
|
+
description,
|
|
6500
|
+
...severity && { severity }
|
|
6501
|
+
};
|
|
6502
|
+
}
|
|
6503
|
+
/**
|
|
6504
|
+
* Checks adapter for Augment Code Review's custom guidelines
|
|
6505
|
+
* (`.augment/code_review_guidelines.yaml`).
|
|
6506
|
+
*
|
|
6507
|
+
* Augment groups review rules into named **areas**: each area carries a
|
|
6508
|
+
* `description`, the `globs` it applies to, and a list of `rules`, every rule
|
|
6509
|
+
* being an `id` / `description` / `severity` triple. Every one of those fields
|
|
6510
|
+
* is required upstream. Rulesync maps one `.rulesync/checks/*.md` onto one rule,
|
|
6511
|
+
* in an area of its own unless an `augmentcode.area` groups several together —
|
|
6512
|
+
* so the whole set collapses into this one file via {@link fromRulesyncChecks}
|
|
6513
|
+
* rather than a file per check.
|
|
6514
|
+
*
|
|
6515
|
+
* **Severity is lossy in one direction.** Augment's scale is `high` / `medium` /
|
|
6516
|
+
* `low` with no band above `high`, so canonical `critical` maps to `high` and
|
|
6517
|
+
* imports back as `high`. A check with no `severity` at all emits `medium`,
|
|
6518
|
+
* since the field cannot be omitted.
|
|
6519
|
+
*
|
|
6520
|
+
* **`file_paths_to_ignore`** is recognized and preserved verbatim, but never
|
|
6521
|
+
* authored or imported: the canonical check model has no ignore surface, and
|
|
6522
|
+
* inventing one here is a separate design question.
|
|
6523
|
+
*
|
|
6524
|
+
* **Generation merges rather than replaces.** Augment's own documentation tells
|
|
6525
|
+
* users to hand-write this file, so only the areas the current check set claims
|
|
6526
|
+
* are rewritten; every other area, `file_paths_to_ignore`, and any unknown
|
|
6527
|
+
* top-level key survive. The cost of that is that rulesync cannot tell its own
|
|
6528
|
+
* leftovers from a hand-written area: renaming a check strands the area under
|
|
6529
|
+
* the old key, and dropping the last Augment-targeting check leaves the areas in
|
|
6530
|
+
* place with a warning rather than guessing which of them to delete. Deleting
|
|
6531
|
+
* the file outright is likewise refused whenever it exists.
|
|
6532
|
+
*
|
|
6533
|
+
* Project scope only — the reviewer reads the file from the committed
|
|
6534
|
+
* repository, and Augment documents no user-level equivalent.
|
|
6535
|
+
*
|
|
6536
|
+
* @see https://docs.augmentcode.com/codereview/review-guidelines
|
|
6537
|
+
*/
|
|
6538
|
+
var AugmentcodeCheck = class AugmentcodeCheck extends ToolCheck {
|
|
6539
|
+
static getSettablePaths(_options = {}) {
|
|
6540
|
+
return {
|
|
6541
|
+
relativeDirPath: AUGMENTCODE_DIR,
|
|
6542
|
+
relativeFilePath: AUGMENTCODE_CODE_REVIEW_GUIDELINES_FILE_NAME
|
|
6543
|
+
};
|
|
6544
|
+
}
|
|
6545
|
+
static isTargetedByRulesyncCheck(rulesyncCheck) {
|
|
6546
|
+
return this.isTargetedByRulesyncCheckDefault({
|
|
6547
|
+
rulesyncCheck,
|
|
6548
|
+
toolTarget: "augmentcode"
|
|
6549
|
+
});
|
|
6550
|
+
}
|
|
6551
|
+
/**
|
|
6552
|
+
* Ownership guard the processor consults before it deletes anything for this
|
|
6553
|
+
* tool. Unlike the Markdown adapters, whose section markers say which text is
|
|
6554
|
+
* rulesync's, YAML carries no such marker — js-yaml drops comments on rewrite,
|
|
6555
|
+
* and an unknown top-level key risks Augment's own parser. So an existing file
|
|
6556
|
+
* is never rulesync's to delete: it may hold hand-written areas, and there is
|
|
6557
|
+
* no way to prove otherwise.
|
|
6558
|
+
*/
|
|
6559
|
+
static async canDeleteAuxiliaryFiles({ outputRoot }) {
|
|
6560
|
+
const paths = AugmentcodeCheck.getSettablePaths();
|
|
6561
|
+
return !await fileExists((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? "code_review_guidelines.yaml"));
|
|
6562
|
+
}
|
|
6563
|
+
static fromRulesyncCheck(_params) {
|
|
6564
|
+
throw new Error("AugmentCode checks are built from all checks at once; use fromRulesyncChecks.");
|
|
6565
|
+
}
|
|
6566
|
+
static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
|
|
6567
|
+
const paths = AugmentcodeCheck.getSettablePaths({ global });
|
|
6568
|
+
const relativeFilePath = paths.relativeFilePath ?? "code_review_guidelines.yaml";
|
|
6569
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
6570
|
+
const existing = parseGuidelines(await readFileContentOrNull(filePath) ?? "", filePath);
|
|
6571
|
+
const existingAreas = isPlainObject$1(existing.areas) ? existing.areas : {};
|
|
6572
|
+
if (rulesyncChecks.length === 0) {
|
|
6573
|
+
if (Object.keys(existingAreas).length > 0) logger?.warn(`AugmentCode checks: no check targets AugmentCode, but ${filePath} still holds review areas. They are left in place — rulesync cannot tell the ones it generated from ones you wrote, so removing them is a manual edit.`);
|
|
6574
|
+
return [];
|
|
6575
|
+
}
|
|
6576
|
+
const entries = rulesyncChecks.map((rulesyncCheck) => ({
|
|
6577
|
+
rulesyncCheck,
|
|
6578
|
+
override: parseOverride$1(rulesyncCheck.getFrontmatter().augmentcode, (0, node_path.join)(RULESYNC_CHECKS_RELATIVE_DIR_PATH, rulesyncCheck.getRelativeFilePath()), logger)
|
|
6579
|
+
}));
|
|
6580
|
+
const generatedAreas = buildAreas({
|
|
6581
|
+
entries,
|
|
6582
|
+
reservedRuleIds: collectRuleIds(existingAreas, new Set(entries.map(({ rulesyncCheck, override }) => areaKeyFor(rulesyncCheck, override))))
|
|
6583
|
+
});
|
|
6584
|
+
const fileContent = (0, js_yaml.dump)({
|
|
6585
|
+
...existing,
|
|
6586
|
+
areas: {
|
|
6587
|
+
...existingAreas,
|
|
6588
|
+
...generatedAreas
|
|
6589
|
+
}
|
|
6590
|
+
}, {
|
|
6591
|
+
lineWidth: -1,
|
|
6592
|
+
noRefs: true
|
|
6593
|
+
});
|
|
6594
|
+
return [new AugmentcodeCheck({
|
|
6595
|
+
outputRoot,
|
|
6596
|
+
relativeDirPath: paths.relativeDirPath,
|
|
6597
|
+
relativeFilePath,
|
|
6598
|
+
fileContent,
|
|
6599
|
+
global
|
|
6600
|
+
})];
|
|
6601
|
+
}
|
|
6602
|
+
static async fromFile({ outputRoot = process.cwd(), global = false }) {
|
|
6603
|
+
const paths = AugmentcodeCheck.getSettablePaths({ global });
|
|
6604
|
+
const relativeFilePath = paths.relativeFilePath ?? "code_review_guidelines.yaml";
|
|
6605
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
6606
|
+
return new AugmentcodeCheck({
|
|
6607
|
+
outputRoot,
|
|
6608
|
+
relativeDirPath: paths.relativeDirPath,
|
|
6609
|
+
relativeFilePath,
|
|
6610
|
+
fileContent: await readFileContentOrNull(filePath) ?? "",
|
|
6611
|
+
global
|
|
6612
|
+
});
|
|
6613
|
+
}
|
|
6614
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
6615
|
+
return new AugmentcodeCheck({
|
|
6616
|
+
outputRoot,
|
|
6617
|
+
relativeDirPath,
|
|
6618
|
+
relativeFilePath,
|
|
6619
|
+
fileContent: "",
|
|
6620
|
+
validate: false,
|
|
6621
|
+
global
|
|
6622
|
+
});
|
|
6623
|
+
}
|
|
6624
|
+
validate() {
|
|
6625
|
+
return {
|
|
6626
|
+
success: true,
|
|
6627
|
+
error: null
|
|
6628
|
+
};
|
|
6629
|
+
}
|
|
6630
|
+
toRulesyncCheck() {
|
|
6631
|
+
const first = this.toRulesyncChecks()[0];
|
|
6632
|
+
if (!first) throw new Error(`No review areas found in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
|
|
6633
|
+
return first;
|
|
6634
|
+
}
|
|
6635
|
+
/**
|
|
6636
|
+
* One check per rule, not per area: an area with three rules is three
|
|
6637
|
+
* independent review instructions, and collapsing them into one check would
|
|
6638
|
+
* make the next generate emit a single merged rule in their place. The area's
|
|
6639
|
+
* key, description and globs ride along in each check's `augmentcode` block,
|
|
6640
|
+
* so regenerating regroups the rules exactly where they were.
|
|
6641
|
+
*/
|
|
6642
|
+
toRulesyncChecks() {
|
|
6643
|
+
const filePath = (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
6644
|
+
const guidelines = parseGuidelines(this.getFileContent(), filePath);
|
|
6645
|
+
const areas = isPlainObject$1(guidelines.areas) ? guidelines.areas : {};
|
|
6646
|
+
const checks = [];
|
|
6647
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
6648
|
+
for (const [areaKey, rawArea] of Object.entries(areas)) {
|
|
6649
|
+
if (!isPlainObject$1(rawArea)) continue;
|
|
6650
|
+
const { description: areaDescription, globs, rules } = readArea(rawArea);
|
|
6651
|
+
for (const rawRule of rules) {
|
|
6652
|
+
const rule = readRule(rawRule);
|
|
6653
|
+
if (!rule) continue;
|
|
6654
|
+
const name = uniqueName(slugifyCheckName(rule.id) || slugifyCheckName(areaKey) || "check", usedNames);
|
|
6655
|
+
checks.push(new RulesyncCheck({
|
|
6656
|
+
outputRoot: ".",
|
|
6657
|
+
relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
|
|
6658
|
+
relativeFilePath: `${name}.md`,
|
|
6659
|
+
frontmatter: {
|
|
6660
|
+
targets: ["*"],
|
|
6661
|
+
...rule.severity && { severity: rule.severity },
|
|
6662
|
+
augmentcode: {
|
|
6663
|
+
area: areaKey,
|
|
6664
|
+
...areaDescription !== void 0 && { areaDescription },
|
|
6665
|
+
...globs && { globs },
|
|
6666
|
+
id: rule.id
|
|
6667
|
+
}
|
|
6668
|
+
},
|
|
6669
|
+
body: rule.description
|
|
6670
|
+
}));
|
|
6671
|
+
}
|
|
6672
|
+
}
|
|
6673
|
+
return checks;
|
|
6674
|
+
}
|
|
6675
|
+
};
|
|
6676
|
+
//#endregion
|
|
6187
6677
|
//#region src/features/checks/aggregated-check-file.ts
|
|
6188
6678
|
/**
|
|
6189
6679
|
* Shared machinery for the tools whose checks surface is **one aggregated
|
|
@@ -7106,6 +7596,22 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
7106
7596
|
}
|
|
7107
7597
|
}
|
|
7108
7598
|
},
|
|
7599
|
+
".github/copilot/settings.json": {
|
|
7600
|
+
format: "json",
|
|
7601
|
+
invalidRootPolicy: "error",
|
|
7602
|
+
features: { permissions: {
|
|
7603
|
+
kind: "replace-owned-keys",
|
|
7604
|
+
ownedKeys: ["deniedUrls"]
|
|
7605
|
+
} }
|
|
7606
|
+
},
|
|
7607
|
+
".copilot/settings.json": {
|
|
7608
|
+
format: "json",
|
|
7609
|
+
invalidRootPolicy: "error",
|
|
7610
|
+
features: { permissions: {
|
|
7611
|
+
kind: "replace-owned-keys",
|
|
7612
|
+
ownedKeys: ["allowedUrls", "deniedUrls"]
|
|
7613
|
+
} }
|
|
7614
|
+
},
|
|
7109
7615
|
".vscode/settings.json": {
|
|
7110
7616
|
format: "jsonc",
|
|
7111
7617
|
invalidRootPolicy: "error",
|
|
@@ -7186,6 +7692,14 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
7186
7692
|
}
|
|
7187
7693
|
}
|
|
7188
7694
|
},
|
|
7695
|
+
".config/muse/settings.json": {
|
|
7696
|
+
format: "json",
|
|
7697
|
+
invalidRootPolicy: "error",
|
|
7698
|
+
features: { mcp: {
|
|
7699
|
+
kind: "replace-owned-keys",
|
|
7700
|
+
ownedKeys: ["mcp_servers", "schema_version"]
|
|
7701
|
+
} }
|
|
7702
|
+
},
|
|
7189
7703
|
".kiro/agents/default.json": {
|
|
7190
7704
|
format: "json",
|
|
7191
7705
|
features: {
|
|
@@ -7833,6 +8347,14 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
|
|
|
7833
8347
|
filePattern: "*.md"
|
|
7834
8348
|
}
|
|
7835
8349
|
}],
|
|
8350
|
+
["augmentcode", {
|
|
8351
|
+
class: AugmentcodeCheck,
|
|
8352
|
+
meta: {
|
|
8353
|
+
supportsGlobal: false,
|
|
8354
|
+
filePattern: AUGMENTCODE_CODE_REVIEW_GUIDELINES_FILE_NAME,
|
|
8355
|
+
committedOutput: true
|
|
8356
|
+
}
|
|
8357
|
+
}],
|
|
7836
8358
|
["cursor", {
|
|
7837
8359
|
class: CursorCheck,
|
|
7838
8360
|
meta: {
|
|
@@ -8620,26 +9142,6 @@ var AntigravityIdeCommand = class extends AntigravitySharedCommand {
|
|
|
8620
9142
|
}
|
|
8621
9143
|
};
|
|
8622
9144
|
//#endregion
|
|
8623
|
-
//#region src/constants/augmentcode-paths.ts
|
|
8624
|
-
const AUGMENTCODE_DIR = ".augment";
|
|
8625
|
-
const AUGMENTCODE_COMMANDS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "commands");
|
|
8626
|
-
/**
|
|
8627
|
-
* The cross-tool `.agents/commands/` root Auggie also discovers commands from.
|
|
8628
|
-
* Auggie resolves commands over `.augment`, `.claude` and `.agents` in both the
|
|
8629
|
-
* workspace and the home directory; rulesync writes to its own `.augment` root
|
|
8630
|
-
* and reads this one as well, mirroring how skills treat `.agents/skills`.
|
|
8631
|
-
* @see https://docs.augmentcode.com/cli/custom-commands
|
|
8632
|
-
*/
|
|
8633
|
-
const AUGMENTCODE_AGENTS_COMMANDS_DIR_PATH = (0, node_path.join)(".agents", "commands");
|
|
8634
|
-
const AUGMENTCODE_SKILLS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "skills");
|
|
8635
|
-
const AUGMENTCODE_AGENTS_DIR_PATH = (0, node_path.join)(AUGMENTCODE_DIR, "agents");
|
|
8636
|
-
const AUGMENTCODE_ALT_AGENTS_DIR_PATH = ".agents";
|
|
8637
|
-
const AUGMENTCODE_AGENTS_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
|
|
8638
|
-
const AUGMENTCODE_SETTINGS_FILE_NAME = "settings.json";
|
|
8639
|
-
const AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
|
|
8640
|
-
const AUGMENTCODE_IGNORE_FILE_NAME = ".augmentignore";
|
|
8641
|
-
const AUGMENTCODE_LEGACY_RULE_FILE_NAME = ".augment-guidelines";
|
|
8642
|
-
//#endregion
|
|
8643
9145
|
//#region src/features/commands/augmentcode-command.ts
|
|
8644
9146
|
const AugmentcodeCommandFrontmatterSchema = zod_mini.z.looseObject({
|
|
8645
9147
|
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -9173,6 +9675,8 @@ const COPILOT_GLOBAL_HOOKS_FILE_NAME = "copilot-ide-hooks.json";
|
|
|
9173
9675
|
const COPILOT_MCP_DIR = ".vscode";
|
|
9174
9676
|
const COPILOT_MCP_FILE_NAME = "mcp.json";
|
|
9175
9677
|
const COPILOT_VSCODE_SETTINGS_FILE_NAME = "settings.json";
|
|
9678
|
+
const COPILOTCLI_SETTINGS_FILE_NAME = "settings.json";
|
|
9679
|
+
const COPILOTCLI_PROJECT_SETTINGS_DIR_PATH = (0, node_path.join)(GITHUB_DIR, "copilot");
|
|
9176
9680
|
const COPILOTCLI_MCP_FILE_NAME = "mcp-config.json";
|
|
9177
9681
|
const COPILOTCLI_PROJECT_MCP_FILE_NAME = "mcp.json";
|
|
9178
9682
|
const COPILOTCLI_AGENTS_DIR_PATH = (0, node_path.join)(COPILOT_DIR, "agents");
|
|
@@ -9432,6 +9936,7 @@ const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
|
|
|
9432
9936
|
const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
|
|
9433
9937
|
const DEVIN_IGNORE_FILE_NAME = ".devinignore";
|
|
9434
9938
|
const DEVIN_LEGACY_IGNORE_FILE_NAME = ".codeiumignore";
|
|
9939
|
+
const DEVIN_WINDSURF_IGNORE_FILE_NAME = ".windsurfignore";
|
|
9435
9940
|
const DEVIN_GLOBAL_IGNORE_DIR_PATH = ".codeium";
|
|
9436
9941
|
const DEVIN_GLOBAL_IGNORE_FILE_NAME = DEVIN_LEGACY_IGNORE_FILE_NAME;
|
|
9437
9942
|
//#endregion
|
|
@@ -9786,7 +10291,7 @@ var GooseCommandConfigFile = class extends ToolFile {
|
|
|
9786
10291
|
* The whole file is a YAML mapping (not frontmatter + markdown body), so the
|
|
9787
10292
|
* class stores the parsed recipe object rather than a frontmatter/body split.
|
|
9788
10293
|
*
|
|
9789
|
-
* @see https://
|
|
10294
|
+
* @see https://goose-docs.ai/docs/guides/recipes/recipe-reference/
|
|
9790
10295
|
*/
|
|
9791
10296
|
const GooseCommandRecipeSchema = zod_mini.z.looseObject({
|
|
9792
10297
|
version: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -13280,7 +13785,7 @@ function collectHandlers({ effectiveHooks, eventMap }) {
|
|
|
13280
13785
|
}
|
|
13281
13786
|
return handlerGroups;
|
|
13282
13787
|
}
|
|
13283
|
-
function buildCommandLines({ handler, usesToolName, blocksToolCall }) {
|
|
13788
|
+
function buildCommandLines$1({ handler, usesToolName, blocksToolCall }) {
|
|
13284
13789
|
const lines = [];
|
|
13285
13790
|
const indent = usesToolName && handler.matcher ? " " : " ";
|
|
13286
13791
|
if (usesToolName && handler.matcher) lines.push(` if (new RegExp(${matcherToEmbeddedLiteral$1(handler.matcher)}).test(event.tool)) {`);
|
|
@@ -13305,7 +13810,7 @@ function buildSubscriptionLines$1(handlerGroups) {
|
|
|
13305
13810
|
const usesToolName = AMP_TOOL_EVENTS.has(ampEvent) && handlers.some((handler) => handler.matcher);
|
|
13306
13811
|
const blocksToolCall = ampEvent === "tool.call";
|
|
13307
13812
|
lines.push(` amp.on(${JSON.stringify(ampEvent)}, async (${usesToolName ? "event, ctx" : "_event, ctx"}) => {`);
|
|
13308
|
-
for (const handler of handlers) lines.push(...buildCommandLines({
|
|
13813
|
+
for (const handler of handlers) lines.push(...buildCommandLines$1({
|
|
13309
13814
|
handler,
|
|
13310
13815
|
usesToolName,
|
|
13311
13816
|
blocksToolCall
|
|
@@ -15290,8 +15795,10 @@ const CopilotHookEntrySchema = zod_mini.z.looseObject({
|
|
|
15290
15795
|
bash: zod_mini.z.optional(zod_mini.z.string()),
|
|
15291
15796
|
powershell: zod_mini.z.optional(zod_mini.z.string()),
|
|
15292
15797
|
command: zod_mini.z.optional(zod_mini.z.string()),
|
|
15798
|
+
cwd: zod_mini.z.optional(zod_mini.z.string()),
|
|
15293
15799
|
env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
15294
|
-
timeoutSec: zod_mini.z.optional(zod_mini.z.number())
|
|
15800
|
+
timeoutSec: zod_mini.z.optional(zod_mini.z.number()),
|
|
15801
|
+
timeout: zod_mini.z.optional(zod_mini.z.number())
|
|
15295
15802
|
});
|
|
15296
15803
|
/**
|
|
15297
15804
|
* Convert canonical hooks config to Copilot format.
|
|
@@ -15371,6 +15878,22 @@ function resolveImportCommand$1(entry, logger) {
|
|
|
15371
15878
|
return typeof entry.command === "string" ? { command: entry.command } : {};
|
|
15372
15879
|
}
|
|
15373
15880
|
/**
|
|
15881
|
+
* Extract the non-command fields preserved across import.
|
|
15882
|
+
*
|
|
15883
|
+
* Generate re-emits any non-canonical key verbatim through `rest`, so a key
|
|
15884
|
+
* dropped here does not survive an import → generate round trip. `cwd` is a
|
|
15885
|
+
* documented Copilot hook field and was previously lost that way.
|
|
15886
|
+
*
|
|
15887
|
+
* @see https://docs.github.com/en/copilot/reference/hooks-reference
|
|
15888
|
+
* @see https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-hooks
|
|
15889
|
+
*/
|
|
15890
|
+
function importPassthrough$1(entry) {
|
|
15891
|
+
const passthrough = {};
|
|
15892
|
+
if (entry.cwd !== void 0) passthrough.cwd = entry.cwd;
|
|
15893
|
+
if (entry.env !== void 0) passthrough.env = entry.env;
|
|
15894
|
+
return passthrough;
|
|
15895
|
+
}
|
|
15896
|
+
/**
|
|
15374
15897
|
* Extract hooks from Copilot hooks JSON into canonical format.
|
|
15375
15898
|
* Copilot format: { version: 1, hooks: { eventName: [...hookEntries] } }
|
|
15376
15899
|
*/
|
|
@@ -15386,13 +15909,13 @@ function copilotHooksToCanonical(copilotHooks, logger) {
|
|
|
15386
15909
|
if (!parseResult.success) continue;
|
|
15387
15910
|
const entry = parseResult.data;
|
|
15388
15911
|
const { command, shell } = resolveImportCommand$1(entry, logger);
|
|
15389
|
-
const timeout = entry.timeoutSec;
|
|
15912
|
+
const timeout = entry.timeoutSec ?? entry.timeout;
|
|
15390
15913
|
defs.push({
|
|
15391
15914
|
type: "command",
|
|
15392
15915
|
...command !== void 0 && { command },
|
|
15393
15916
|
...shell !== void 0 && { shell },
|
|
15394
|
-
...
|
|
15395
|
-
...
|
|
15917
|
+
...timeout !== void 0 && { timeout },
|
|
15918
|
+
...importPassthrough$1(entry)
|
|
15396
15919
|
});
|
|
15397
15920
|
}
|
|
15398
15921
|
if (defs.length > 0) canonical[eventName] = defs;
|
|
@@ -15675,19 +16198,19 @@ function importPassthrough(entry) {
|
|
|
15675
16198
|
* A shell-specific field carries its `shell` through so re-export writes the
|
|
15676
16199
|
* same field back. An entry using only the portable `command` field leaves
|
|
15677
16200
|
* `shell` unset, which re-export renders as the portable field again.
|
|
16201
|
+
*
|
|
16202
|
+
* When both shell-specific fields are present, `bash` wins and a warning names
|
|
16203
|
+
* the ignored `powershell`. The choice is deliberately not platform-dependent:
|
|
16204
|
+
* importing on Windows must not produce a different canonical config than
|
|
16205
|
+
* importing the same file on Linux, which would make the rulesync hooks file
|
|
16206
|
+
* differ per machine for anyone who checks it in.
|
|
15678
16207
|
*/
|
|
15679
16208
|
function resolveImportCommand(entry, logger) {
|
|
15680
16209
|
const hasBash = typeof entry.bash === "string";
|
|
15681
16210
|
const hasPowershell = typeof entry.powershell === "string";
|
|
15682
16211
|
if (hasBash && hasPowershell) {
|
|
15683
|
-
|
|
15684
|
-
|
|
15685
|
-
const ignored = isWindows ? "bash" : "powershell";
|
|
15686
|
-
logger?.warn(`Copilot CLI hook has both bash and powershell commands; using ${chosen} and ignoring ${ignored} on this platform.`);
|
|
15687
|
-
return isWindows ? {
|
|
15688
|
-
command: entry.powershell,
|
|
15689
|
-
shell: "powershell"
|
|
15690
|
-
} : {
|
|
16212
|
+
logger?.warn("Copilot CLI hook has both bash and powershell commands; using bash and ignoring powershell, so the imported config does not depend on the machine the import ran on.");
|
|
16213
|
+
return {
|
|
15691
16214
|
command: entry.bash,
|
|
15692
16215
|
shell: "bash"
|
|
15693
16216
|
};
|
|
@@ -16346,7 +16869,7 @@ const GOOSE_CONVERTER_CONFIG = {
|
|
|
16346
16869
|
*
|
|
16347
16870
|
* The JSON shape matches Claude Code's: each PascalCase event maps to an array of
|
|
16348
16871
|
* `{ matcher, hooks: [{ type: "command", command }] }` entries.
|
|
16349
|
-
* @see https://
|
|
16872
|
+
* @see https://goose-docs.ai/docs/guides/context-engineering/hooks/
|
|
16350
16873
|
*/
|
|
16351
16874
|
var GooseHooks = class GooseHooks extends ToolHooks {
|
|
16352
16875
|
constructor(params) {
|
|
@@ -17255,6 +17778,38 @@ function stripTrustedDirectoryWrapper(command) {
|
|
|
17255
17778
|
if (posix?.[1]) return posix[1];
|
|
17256
17779
|
return command.match(/^set "RULESYNC_KIMI_HOOK_CWD=1" && cd \/d "(?:""|[^"])*" && ([\s\S]*)$/)?.[1] ?? command;
|
|
17257
17780
|
}
|
|
17781
|
+
/**
|
|
17782
|
+
* Native Kimi Code events whose Event Reference row lists the matcher as
|
|
17783
|
+
* "Empty string". Kimi Code documents `matcher` as "a regular expression to
|
|
17784
|
+
* filter event targets; if omitted, matches all", so on these events the
|
|
17785
|
+
* regex is tested against `""`: any non-trivial matcher simply never matches
|
|
17786
|
+
* and the hook silently never runs. Dropping the matcher is what makes the
|
|
17787
|
+
* hook fire at all, which is the authored intent — these events have no target
|
|
17788
|
+
* to filter on in the first place.
|
|
17789
|
+
*
|
|
17790
|
+
* Keyed on native names because the check runs after the canonical → native
|
|
17791
|
+
* mapping: `SessionHeartbeat` and `Interrupt` have no canonical counterpart and
|
|
17792
|
+
* are only reachable through a per-tool `kimi-code` override naming them
|
|
17793
|
+
* directly.
|
|
17794
|
+
*
|
|
17795
|
+
* Deliberately narrower than Claude Code's equivalent set: Kimi Code's
|
|
17796
|
+
* `UserPromptSubmit` matches the submitted prompt text, and `PermissionResult`
|
|
17797
|
+
* matches the tool name, so a matcher on either is meaningful and is kept.
|
|
17798
|
+
*
|
|
17799
|
+
* @see https://moonshotai.github.io/kimi-code/en/customization/hooks.html
|
|
17800
|
+
*/
|
|
17801
|
+
const KIMI_CODE_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set([
|
|
17802
|
+
"Stop",
|
|
17803
|
+
"SessionHeartbeat",
|
|
17804
|
+
"Interrupt"
|
|
17805
|
+
]);
|
|
17806
|
+
/** Resolve the `matcher` part of an emitted entry, dropping dead matchers. */
|
|
17807
|
+
function resolveMatcherPart({ matcher, nativeEvent, logger }) {
|
|
17808
|
+
if (!matcher) return {};
|
|
17809
|
+
if (!KIMI_CODE_NO_MATCHER_EVENTS.has(nativeEvent)) return { matcher };
|
|
17810
|
+
logger?.warn(`matcher "${matcher}" on "${nativeEvent}" hook will be ignored — this event does not support matchers`);
|
|
17811
|
+
return {};
|
|
17812
|
+
}
|
|
17258
17813
|
function buildEffectiveHooks(config, toolOverrideHooks) {
|
|
17259
17814
|
const supported = new Set(KIMI_CODE_HOOK_EVENTS);
|
|
17260
17815
|
const shared = {};
|
|
@@ -17284,7 +17839,11 @@ function canonicalToKimiCodeHooks({ config, toolOverrideHooks, trustedDirectory,
|
|
|
17284
17839
|
command: definition.command,
|
|
17285
17840
|
trustedDirectory
|
|
17286
17841
|
}),
|
|
17287
|
-
...
|
|
17842
|
+
...resolveMatcherPart({
|
|
17843
|
+
matcher: definition.matcher,
|
|
17844
|
+
nativeEvent,
|
|
17845
|
+
logger
|
|
17846
|
+
}),
|
|
17288
17847
|
...validTimeout && timeout !== void 0 && { timeout }
|
|
17289
17848
|
});
|
|
17290
17849
|
}
|
|
@@ -17482,17 +18041,33 @@ function buildKiroIdeEntriesForEvent(trigger, definitions) {
|
|
|
17482
18041
|
}
|
|
17483
18042
|
return entries;
|
|
17484
18043
|
}
|
|
17485
|
-
|
|
18044
|
+
/**
|
|
18045
|
+
* The single `HooksConfig` key every Kiro target reads its tool-specific
|
|
18046
|
+
* overrides from.
|
|
18047
|
+
*
|
|
18048
|
+
* `kiro-ide` and `kiro-cli` write the same `.kiro/hooks/rulesync.json` at both
|
|
18049
|
+
* scopes, so per-target override blocks would make that one file's content
|
|
18050
|
+
* depend on generation order (last writer wins). All Kiro variants therefore
|
|
18051
|
+
* share the `kiro` block — the same resolution the Kiro MCP and permissions
|
|
18052
|
+
* wiring already use for the file they share.
|
|
18053
|
+
*/
|
|
18054
|
+
const KIRO_HOOKS_OVERRIDE_KEY = "kiro";
|
|
18055
|
+
/**
|
|
18056
|
+
* Override keys a user might reach for that nothing reads, mapped to the key
|
|
18057
|
+
* that is actually read.
|
|
18058
|
+
*/
|
|
18059
|
+
const KIRO_HOOKS_IGNORED_OVERRIDE_KEYS = ["kiro-ide", "kiro-cli"];
|
|
18060
|
+
function canonicalToKiroIdeHooks(config) {
|
|
17486
18061
|
const kiroIdeSupported = new Set(KIRO_IDE_HOOK_EVENTS);
|
|
17487
18062
|
const sharedHooks = {};
|
|
17488
18063
|
for (const [event, defs] of Object.entries(config.hooks)) if (kiroIdeSupported.has(event)) sharedHooks[event] = defs;
|
|
17489
18064
|
const effectiveHooks = {
|
|
17490
18065
|
...sharedHooks,
|
|
17491
|
-
...config[
|
|
18066
|
+
...config[KIRO_HOOKS_OVERRIDE_KEY]?.hooks
|
|
17492
18067
|
};
|
|
17493
18068
|
const entries = [];
|
|
17494
18069
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
17495
|
-
const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? eventName;
|
|
18070
|
+
const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? KIRO_LEGACY_TO_KIRO_IDE_TRIGGER_NAMES[eventName] ?? eventName;
|
|
17496
18071
|
entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
|
|
17497
18072
|
}
|
|
17498
18073
|
return entries;
|
|
@@ -17537,6 +18112,11 @@ function kiroIdeHooksToCanonical(entries) {
|
|
|
17537
18112
|
* still writes the embedded `.kiro/agents/default.json` agent-config shape,
|
|
17538
18113
|
* which Kiro CLI 3.0 no longer reads.
|
|
17539
18114
|
*
|
|
18115
|
+
* Because both targets write the very same file, they resolve their
|
|
18116
|
+
* tool-specific overrides from one shared block
|
|
18117
|
+
* ({@link KIRO_HOOKS_OVERRIDE_KEY}) rather than per-target blocks, so
|
|
18118
|
+
* generating either or both targets always yields the same file.
|
|
18119
|
+
*
|
|
17540
18120
|
* @see https://kiro.dev/docs/hooks/
|
|
17541
18121
|
*/
|
|
17542
18122
|
var KiroIdeHooks = class extends ToolHooks {
|
|
@@ -17549,14 +18129,6 @@ var KiroIdeHooks = class extends ToolHooks {
|
|
|
17549
18129
|
}, null, 2)
|
|
17550
18130
|
});
|
|
17551
18131
|
}
|
|
17552
|
-
/**
|
|
17553
|
-
* The `HooksConfig` key whose `hooks` block provides tool-specific overrides
|
|
17554
|
-
* for this target. {@link import("./kiro-cli-hooks.js").KiroCliHooks}
|
|
17555
|
-
* overrides this to `kiro-cli`.
|
|
17556
|
-
*/
|
|
17557
|
-
static getOverrideKey() {
|
|
17558
|
-
return "kiro-ide";
|
|
17559
|
-
}
|
|
17560
18132
|
static getSettablePaths(_options = {}) {
|
|
17561
18133
|
return {
|
|
17562
18134
|
relativeDirPath: KIRO_IDE_HOOKS_DIR_PATH,
|
|
@@ -17577,9 +18149,14 @@ var KiroIdeHooks = class extends ToolHooks {
|
|
|
17577
18149
|
validate
|
|
17578
18150
|
});
|
|
17579
18151
|
}
|
|
17580
|
-
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
|
|
18152
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
17581
18153
|
const paths = this.getSettablePaths({ global });
|
|
17582
|
-
const
|
|
18154
|
+
const config = rulesyncHooks.getJson();
|
|
18155
|
+
for (const ignoredKey of KIRO_HOOKS_IGNORED_OVERRIDE_KEYS) {
|
|
18156
|
+
if (config[ignoredKey]?.hooks === void 0) continue;
|
|
18157
|
+
logger?.warn(`The "${ignoredKey}.hooks" block in ${(0, node_path.join)(rulesyncHooks.getRelativeDirPath(), rulesyncHooks.getRelativeFilePath())} is ignored. Author it under the "${KIRO_HOOKS_OVERRIDE_KEY}.hooks" key instead: the Kiro IDE and Kiro CLI targets write the same hooks file, so they read one shared block.`);
|
|
18158
|
+
}
|
|
18159
|
+
const hooks = canonicalToKiroIdeHooks(config);
|
|
17583
18160
|
const fileContent = JSON.stringify({
|
|
17584
18161
|
version: "v1",
|
|
17585
18162
|
hooks
|
|
@@ -17600,10 +18177,9 @@ var KiroIdeHooks = class extends ToolHooks {
|
|
|
17600
18177
|
throw new Error(`Failed to parse Kiro IDE hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
17601
18178
|
}
|
|
17602
18179
|
const hooks = kiroIdeHooksToCanonical(parsed.hooks ?? []);
|
|
17603
|
-
const overrideKey = this.constructor.getOverrideKey();
|
|
17604
18180
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
17605
18181
|
hooks,
|
|
17606
|
-
overrideKey
|
|
18182
|
+
overrideKey: KIRO_HOOKS_OVERRIDE_KEY
|
|
17607
18183
|
}), null, 2) });
|
|
17608
18184
|
}
|
|
17609
18185
|
validate() {
|
|
@@ -17631,9 +18207,13 @@ var KiroIdeHooks = class extends ToolHooks {
|
|
|
17631
18207
|
* Hooks generator for the **Kiro CLI**.
|
|
17632
18208
|
*
|
|
17633
18209
|
* Kiro CLI 3.0 reads the same standalone `.kiro/hooks/*.json` v1 format the
|
|
17634
|
-
* Kiro IDE reads
|
|
17635
|
-
*
|
|
17636
|
-
*
|
|
18210
|
+
* Kiro IDE reads — the same directory, and in rulesync the same
|
|
18211
|
+
* `.kiro/hooks/rulesync.json` file — so this is {@link KiroIdeHooks} under a
|
|
18212
|
+
* second target name, down to the shared `kiro` override block both resolve
|
|
18213
|
+
* their tool-specific hooks from. A per-target override block would make that
|
|
18214
|
+
* one file's content depend on which target was generated last, which is
|
|
18215
|
+
* exactly what the shared block avoids (the Kiro MCP and permissions wiring
|
|
18216
|
+
* resolve their shared files the same way).
|
|
17637
18217
|
*
|
|
17638
18218
|
* The embedded `.kiro/agents/default.json` agent-hook format this target used
|
|
17639
18219
|
* to emit is documented as not working in 3.0, so it is left to the deprecated
|
|
@@ -17642,11 +18222,7 @@ var KiroIdeHooks = class extends ToolHooks {
|
|
|
17642
18222
|
* @see https://kiro.dev/docs/cli/v3/hooks-migration/
|
|
17643
18223
|
* @see https://kiro.dev/docs/hooks/
|
|
17644
18224
|
*/
|
|
17645
|
-
var KiroCliHooks = class extends KiroIdeHooks {
|
|
17646
|
-
static getOverrideKey() {
|
|
17647
|
-
return "kiro-cli";
|
|
17648
|
-
}
|
|
17649
|
-
};
|
|
18225
|
+
var KiroCliHooks = class extends KiroIdeHooks {};
|
|
17650
18226
|
//#endregion
|
|
17651
18227
|
//#region src/features/hooks/kiro-hooks.ts
|
|
17652
18228
|
/**
|
|
@@ -17671,14 +18247,30 @@ function buildKiroEntriesForEvent(definitions) {
|
|
|
17671
18247
|
}
|
|
17672
18248
|
return entries;
|
|
17673
18249
|
}
|
|
17674
|
-
|
|
18250
|
+
/**
|
|
18251
|
+
* Event keys the embedded agent-config format defines: the canonical events it
|
|
18252
|
+
* supports plus its own native spellings (`agentSpawn`, `userPromptSubmit`, …).
|
|
18253
|
+
*
|
|
18254
|
+
* The `kiro` override block is shared with the standalone `.kiro/hooks/*.json`
|
|
18255
|
+
* targets, whose vocabulary is different (`PostFileSave`, `PreTaskExec`, …).
|
|
18256
|
+
* Passing those through here would write event keys Kiro does not define into
|
|
18257
|
+
* `.kiro/agents/default.json`, so they are dropped instead.
|
|
18258
|
+
* @see https://kiro.dev/docs/cli/v3/hooks-migration/
|
|
18259
|
+
*/
|
|
18260
|
+
const KIRO_AGENT_CONFIG_EVENT_KEYS = /* @__PURE__ */ new Set([...KIRO_HOOK_EVENTS, ...KIRO_AGENT_CONFIG_NATIVE_EVENT_NAMES]);
|
|
18261
|
+
function canonicalToKiroHooks({ config, logger }) {
|
|
17675
18262
|
const overrideKey = "kiro";
|
|
17676
18263
|
const kiroSupported = new Set(KIRO_HOOK_EVENTS);
|
|
17677
18264
|
const sharedHooks = {};
|
|
17678
18265
|
for (const [event, defs] of Object.entries(config.hooks)) if (kiroSupported.has(event)) sharedHooks[event] = defs;
|
|
18266
|
+
const overrideHooks = {};
|
|
18267
|
+
const droppedEvents = [];
|
|
18268
|
+
for (const [event, defs] of Object.entries(config[overrideKey]?.hooks ?? {})) if (KIRO_AGENT_CONFIG_EVENT_KEYS.has(event)) overrideHooks[event] = defs;
|
|
18269
|
+
else droppedEvents.push(event);
|
|
18270
|
+
if (droppedEvents.length > 0) logger?.warn(`Skipped hook event(s) from the "kiro" override block for the deprecated kiro agent config (no event key of that format): ${droppedEvents.join(", ")}. They are emitted for the kiro-cli / kiro-ide targets, which read the same block.`);
|
|
17679
18271
|
const effectiveHooks = {
|
|
17680
18272
|
...sharedHooks,
|
|
17681
|
-
...
|
|
18273
|
+
...overrideHooks
|
|
17682
18274
|
};
|
|
17683
18275
|
const kiro = {};
|
|
17684
18276
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
@@ -17773,11 +18365,14 @@ var KiroHooks = class KiroHooks extends ToolHooks {
|
|
|
17773
18365
|
validate
|
|
17774
18366
|
});
|
|
17775
18367
|
}
|
|
17776
|
-
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
|
|
18368
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
17777
18369
|
const paths = KiroHooks.getSettablePaths({ global });
|
|
17778
18370
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
17779
18371
|
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
17780
|
-
const kiroHooks = canonicalToKiroHooks(
|
|
18372
|
+
const kiroHooks = canonicalToKiroHooks({
|
|
18373
|
+
config: rulesyncHooks.getJson(),
|
|
18374
|
+
logger
|
|
18375
|
+
});
|
|
17781
18376
|
const fileContent = applySharedConfigPatch({
|
|
17782
18377
|
fileKey: sharedConfigFileKey(paths),
|
|
17783
18378
|
feature: "hooks",
|
|
@@ -17893,6 +18488,30 @@ const PI_TOOL_EVENTS = /* @__PURE__ */ new Set(["tool_call", "tool_result"]);
|
|
|
17893
18488
|
*/
|
|
17894
18489
|
const PI_ASSISTANT_MESSAGE_EVENTS = /* @__PURE__ */ new Set(["message_end"]);
|
|
17895
18490
|
/**
|
|
18491
|
+
* `tool_call` is the only Pi extension event that can block, and it is Pi's
|
|
18492
|
+
* only tool gate. Its return contract is
|
|
18493
|
+
* `{ block: true, reason?: string, terminate?: boolean }`.
|
|
18494
|
+
*
|
|
18495
|
+
* @see https://github.com/earendil-works/pi/blob/v0.84.1/packages/coding-agent/docs/extensions.md#tool_call
|
|
18496
|
+
*/
|
|
18497
|
+
const PI_BLOCKING_EVENT = "tool_call";
|
|
18498
|
+
/**
|
|
18499
|
+
* Helper emitted alongside blocking handlers. `promisify(exec)` rejects on a
|
|
18500
|
+
* non-zero exit with an error carrying `stdout` / `stderr` / `code`, so the
|
|
18501
|
+
* reason is derived from the rejection rather than from a resolved exit code.
|
|
18502
|
+
*/
|
|
18503
|
+
const BLOCK_REASON_HELPER_LINES = [
|
|
18504
|
+
"function toBlockReason(error: unknown): string {",
|
|
18505
|
+
" const result = error as { stdout?: unknown; stderr?: unknown; code?: unknown } | null;",
|
|
18506
|
+
" const stderr = String(result?.stderr ?? \"\").trim();",
|
|
18507
|
+
" if (stderr) return stderr;",
|
|
18508
|
+
" const stdout = String(result?.stdout ?? \"\").trim();",
|
|
18509
|
+
" if (stdout) return stdout;",
|
|
18510
|
+
" if (result?.code !== undefined) return `Hook command failed with exit code ${result.code}.`;",
|
|
18511
|
+
" return error instanceof Error ? error.message : String(error);",
|
|
18512
|
+
"}"
|
|
18513
|
+
];
|
|
18514
|
+
/**
|
|
17896
18515
|
* Validate a hook matcher as a regular expression and return it as a JS
|
|
17897
18516
|
* string-literal (JSON.stringify quoting) safe to embed in generated code.
|
|
17898
18517
|
*/
|
|
@@ -17929,6 +18548,22 @@ function collectPiHandlers({ effectiveHooks, eventMap }) {
|
|
|
17929
18548
|
}
|
|
17930
18549
|
return handlerGroups;
|
|
17931
18550
|
}
|
|
18551
|
+
function buildCommandLines({ handler, usesToolName, blocksToolCall }) {
|
|
18552
|
+
const lines = [];
|
|
18553
|
+
const gated = usesToolName && Boolean(handler.matcher);
|
|
18554
|
+
const indent = gated ? " " : " ";
|
|
18555
|
+
const embeddedCommand = JSON.stringify(handler.command);
|
|
18556
|
+
if (gated && handler.matcher) lines.push(` if (new RegExp(${matcherToEmbeddedLiteral(handler.matcher)}).test(event.toolName)) {`);
|
|
18557
|
+
if (blocksToolCall) {
|
|
18558
|
+
lines.push(`${indent}try {`);
|
|
18559
|
+
lines.push(`${indent} await run(${embeddedCommand});`);
|
|
18560
|
+
lines.push(`${indent}} catch (error) {`);
|
|
18561
|
+
lines.push(`${indent} return { block: true, reason: toBlockReason(error) };`);
|
|
18562
|
+
lines.push(`${indent}}`);
|
|
18563
|
+
} else lines.push(`${indent}await run(${embeddedCommand});`);
|
|
18564
|
+
if (gated) lines.push(" }");
|
|
18565
|
+
return lines;
|
|
18566
|
+
}
|
|
17932
18567
|
function buildSubscriptionLines(handlerGroups) {
|
|
17933
18568
|
const lines = [];
|
|
17934
18569
|
for (const [piEvent, handlers] of Object.entries(handlerGroups)) {
|
|
@@ -17937,14 +18572,11 @@ function buildSubscriptionLines(handlerGroups) {
|
|
|
17937
18572
|
const usesEvent = usesToolName || gatesOnAssistant;
|
|
17938
18573
|
lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${usesEvent ? "event" : ""}) => {`);
|
|
17939
18574
|
if (gatesOnAssistant) lines.push(` if (event.message.role !== "assistant") return;`);
|
|
17940
|
-
for (const handler of handlers) {
|
|
17941
|
-
|
|
17942
|
-
|
|
17943
|
-
|
|
17944
|
-
|
|
17945
|
-
lines.push(" }");
|
|
17946
|
-
} else lines.push(` await run(${embeddedCommand});`);
|
|
17947
|
-
}
|
|
18575
|
+
for (const handler of handlers) lines.push(...buildCommandLines({
|
|
18576
|
+
handler,
|
|
18577
|
+
usesToolName,
|
|
18578
|
+
blocksToolCall: piEvent === PI_BLOCKING_EVENT
|
|
18579
|
+
}));
|
|
17948
18580
|
lines.push(" });");
|
|
17949
18581
|
}
|
|
17950
18582
|
return lines;
|
|
@@ -17953,8 +18585,9 @@ function buildSubscriptionLines(handlerGroups) {
|
|
|
17953
18585
|
* Generate the rulesync-owned Pi extension (a TypeScript module with a
|
|
17954
18586
|
* default-export factory receiving Pi's ExtensionAPI) that subscribes to the
|
|
17955
18587
|
* mapped lifecycle events and executes the configured hook commands via the
|
|
17956
|
-
* platform shell.
|
|
17957
|
-
*
|
|
18588
|
+
* platform shell. Handlers observe events, except on `tool_call` — Pi's only
|
|
18589
|
+
* blocking event and its only tool gate — where a hook command that exits
|
|
18590
|
+
* non-zero denies the call with `{ block: true, reason }`.
|
|
17958
18591
|
*
|
|
17959
18592
|
* @see https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md
|
|
17960
18593
|
*/
|
|
@@ -17966,10 +18599,12 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap }) {
|
|
|
17966
18599
|
};
|
|
17967
18600
|
const effectiveHooks = {};
|
|
17968
18601
|
for (const [event, defs] of Object.entries(configHooks)) if (supported.has(event)) effectiveHooks[event] = defs;
|
|
17969
|
-
const
|
|
18602
|
+
const handlerGroups = collectPiHandlers({
|
|
17970
18603
|
effectiveHooks,
|
|
17971
18604
|
eventMap
|
|
17972
|
-
})
|
|
18605
|
+
});
|
|
18606
|
+
const subscriptionLines = buildSubscriptionLines(handlerGroups);
|
|
18607
|
+
const needsBlockReasonHelper = Boolean(handlerGroups[PI_BLOCKING_EVENT]);
|
|
17973
18608
|
const lines = ["// Generated by rulesync. Do not edit manually."];
|
|
17974
18609
|
if (subscriptionLines.length === 0) {
|
|
17975
18610
|
lines.push("export default function () {}");
|
|
@@ -17983,6 +18618,10 @@ function generatePiExtensionCode({ config, supportedEvents, eventMap }) {
|
|
|
17983
18618
|
lines.push("");
|
|
17984
18619
|
lines.push("const run = promisify(exec);");
|
|
17985
18620
|
lines.push("");
|
|
18621
|
+
if (needsBlockReasonHelper) {
|
|
18622
|
+
lines.push(...BLOCK_REASON_HELPER_LINES);
|
|
18623
|
+
lines.push("");
|
|
18624
|
+
}
|
|
17986
18625
|
lines.push("export default function (pi: ExtensionAPI) {");
|
|
17987
18626
|
lines.push(...subscriptionLines);
|
|
17988
18627
|
lines.push("}");
|
|
@@ -18712,6 +19351,20 @@ function unsupportedMatcherEventNames({ factory, effectiveHooks }) {
|
|
|
18712
19351
|
}
|
|
18713
19352
|
return [...eventsWithMatcher];
|
|
18714
19353
|
}
|
|
19354
|
+
/**
|
|
19355
|
+
* Targets that read their tool-scoped `{key}.hooks` override from a differently
|
|
19356
|
+
* named key. The two Kiro standalone-format targets write the very same
|
|
19357
|
+
* `.kiro/hooks/rulesync.json`, so they share the `kiro` block: a per-target
|
|
19358
|
+
* block would make that one file's content depend on generation order. The same
|
|
19359
|
+
* resolution is used by the MCP (`MCP_BLOCK_KEY_ALIASES`) and permissions
|
|
19360
|
+
* (`PERMISSION_OVERRIDE_KEY_ALIASES`) features for the files they share.
|
|
19361
|
+
*/
|
|
19362
|
+
const HOOKS_OVERRIDE_KEY_ALIASES = {
|
|
19363
|
+
"kiro-cli": KIRO_HOOKS_OVERRIDE_KEY,
|
|
19364
|
+
"kiro-ide": KIRO_HOOKS_OVERRIDE_KEY
|
|
19365
|
+
};
|
|
19366
|
+
/** The targets writing the standalone `.kiro/hooks/*.json` v1 format. */
|
|
19367
|
+
const KIRO_STANDALONE_HOOKS_TARGETS = /* @__PURE__ */ new Set(["kiro-cli", "kiro-ide"]);
|
|
18715
19368
|
const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
18716
19369
|
["amp", {
|
|
18717
19370
|
class: AmpHooks,
|
|
@@ -18952,7 +19605,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
18952
19605
|
},
|
|
18953
19606
|
supportedEvents: KIRO_HOOK_EVENTS,
|
|
18954
19607
|
supportedHookTypes: ["command"],
|
|
18955
|
-
supportsMatcher: true
|
|
19608
|
+
supportsMatcher: true,
|
|
19609
|
+
passthroughOverrideEvents: true
|
|
18956
19610
|
}],
|
|
18957
19611
|
["kiro-cli", {
|
|
18958
19612
|
class: KiroCliHooks,
|
|
@@ -19132,7 +19786,7 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
19132
19786
|
if (!factory) throw new Error(`Unsupported tool target: ${this.toolTarget}`);
|
|
19133
19787
|
const config = rulesyncHooks.getJson();
|
|
19134
19788
|
const sharedHooks = config.hooks;
|
|
19135
|
-
const overrideHooks = config[this.toolTarget]?.hooks ?? {};
|
|
19789
|
+
const overrideHooks = config[HOOKS_OVERRIDE_KEY_ALIASES[this.toolTarget] ?? this.toolTarget]?.hooks ?? {};
|
|
19136
19790
|
const effectiveHooks = {
|
|
19137
19791
|
...sharedHooks,
|
|
19138
19792
|
...overrideHooks
|
|
@@ -19158,14 +19812,14 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
19158
19812
|
}
|
|
19159
19813
|
for (const [hookType, events] of unsupportedTypeToEvents) this.logger.warn(`Skipped ${hookType}-type hook(s) for ${this.toolTarget} (not supported): ${Array.from(events).join(", ")}`);
|
|
19160
19814
|
}
|
|
19161
|
-
if (this.toolTarget
|
|
19815
|
+
if (!KIRO_STANDALONE_HOOKS_TARGETS.has(this.toolTarget)) {
|
|
19162
19816
|
const skippedEvents = new Set(unsupportedEventNames({
|
|
19163
19817
|
factory,
|
|
19164
19818
|
sharedHooks,
|
|
19165
19819
|
effectiveHooks
|
|
19166
19820
|
}));
|
|
19167
19821
|
const eventsWithDisabledHooks = Object.entries(sharedHooks).filter(([event, defs]) => !skippedEvents.has(event) && defs.some((def) => def.enabled === false)).map(([event]) => event);
|
|
19168
|
-
if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only kiro-ide supports the flag): ${eventsWithDisabledHooks.join(", ")}`);
|
|
19822
|
+
if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only the kiro-cli / kiro-ide standalone hooks format supports the flag): ${eventsWithDisabledHooks.join(", ")}`);
|
|
19169
19823
|
}
|
|
19170
19824
|
const eventsWithUnsupportedMatcher = unsupportedMatcherEventNames({
|
|
19171
19825
|
factory,
|
|
@@ -19664,9 +20318,11 @@ var CursorIgnore = class CursorIgnore extends ToolIgnore {
|
|
|
19664
20318
|
*
|
|
19665
20319
|
* Generates the brand-aligned `.devinignore` file with gitignore-compatible
|
|
19666
20320
|
* syntax. Devin automatically respects `.gitignore` patterns and has built-in
|
|
19667
|
-
* defaults for node_modules/ and hidden files. On import, the
|
|
19668
|
-
* `.codeiumignore`
|
|
19669
|
-
* round-trip.
|
|
20321
|
+
* defaults for node_modules/ and hidden files. On import, the pre-rebrand
|
|
20322
|
+
* `.codeiumignore` and `.windsurfignore` filenames are read as fallbacks so
|
|
20323
|
+
* existing projects still round-trip. The docs list the three names side by
|
|
20324
|
+
* side without defining a precedence between them, so the order below is
|
|
20325
|
+
* rulesync's own choice: brand-aligned name first, then the legacy names.
|
|
19670
20326
|
*
|
|
19671
20327
|
* In global mode the enterprise-wide `~/.codeium/.codeiumignore` is written
|
|
19672
20328
|
* instead; see `DEVIN_GLOBAL_IGNORE_DIR_PATH` for why that path keeps the
|
|
@@ -19705,9 +20361,16 @@ var DevinIgnore = class DevinIgnore extends ToolIgnore {
|
|
|
19705
20361
|
validate,
|
|
19706
20362
|
global
|
|
19707
20363
|
});
|
|
19708
|
-
const
|
|
19709
|
-
|
|
19710
|
-
|
|
20364
|
+
const candidateFileNames = [
|
|
20365
|
+
relativeFilePath,
|
|
20366
|
+
DEVIN_LEGACY_IGNORE_FILE_NAME,
|
|
20367
|
+
DEVIN_WINDSURF_IGNORE_FILE_NAME
|
|
20368
|
+
];
|
|
20369
|
+
let resolvedFilePath = relativeFilePath;
|
|
20370
|
+
for (const candidateFileName of candidateFileNames) if (await fileExists((0, node_path.join)(outputRoot, relativeDirPath, candidateFileName))) {
|
|
20371
|
+
resolvedFilePath = candidateFileName;
|
|
20372
|
+
break;
|
|
20373
|
+
}
|
|
19711
20374
|
const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativeDirPath, resolvedFilePath));
|
|
19712
20375
|
return new DevinIgnore({
|
|
19713
20376
|
outputRoot,
|
|
@@ -22936,8 +23599,8 @@ function convertToGoosePluginMcpServers(mcpServers, logger) {
|
|
|
22936
23599
|
* shape and cannot express `url`/`headers`, so remote servers are skipped with
|
|
22937
23600
|
* a warning in project mode (use `--global` to sync them instead).
|
|
22938
23601
|
*
|
|
22939
|
-
* @see https://
|
|
22940
|
-
* @see https://github.com/
|
|
23602
|
+
* @see https://goose-docs.ai/docs/getting-started/using-extensions/
|
|
23603
|
+
* @see https://github.com/aaif-goose/goose/pull/9471
|
|
22941
23604
|
*/
|
|
22942
23605
|
var GooseMcp = class GooseMcp extends ToolMcp {
|
|
22943
23606
|
config;
|
|
@@ -24441,6 +25104,213 @@ var KiroMcp = class KiroMcp extends ToolMcp {
|
|
|
24441
25104
|
}
|
|
24442
25105
|
};
|
|
24443
25106
|
//#endregion
|
|
25107
|
+
//#region src/constants/musecode-paths.ts
|
|
25108
|
+
const MUSECODE_RULE_FILE_NAME = "AGENTS.md";
|
|
25109
|
+
const MUSECODE_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
|
|
25110
|
+
const MUSECODE_GLOBAL_CONFIG_DIR_PATH = (0, node_path.join)(".config", "muse");
|
|
25111
|
+
const MUSECODE_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(MUSECODE_GLOBAL_CONFIG_DIR_PATH, "skills");
|
|
25112
|
+
const MUSECODE_SETTINGS_FILE_NAME = "settings.json";
|
|
25113
|
+
//#endregion
|
|
25114
|
+
//#region src/features/mcp/musecode-mcp.ts
|
|
25115
|
+
const MUSECODE_GLOBAL_ONLY_MESSAGE = "Muse Code MCP is global-only; use --global to sync ~/.config/muse/settings.json";
|
|
25116
|
+
/**
|
|
25117
|
+
* Single spelling of the settings.json codec/policy, matching the
|
|
25118
|
+
* `SHARED_CONFIG_OWNERSHIP` declaration for `.config/muse/settings.json`:
|
|
25119
|
+
* fail closed on an unparseable root rather than replacing the user's primary
|
|
25120
|
+
* Muse Code config with generated output.
|
|
25121
|
+
*/
|
|
25122
|
+
function parseMusecodeSettings(fileContent, filePath) {
|
|
25123
|
+
return parseSharedConfig({
|
|
25124
|
+
format: "json",
|
|
25125
|
+
fileContent,
|
|
25126
|
+
filePath,
|
|
25127
|
+
invalidRootPolicy: "error"
|
|
25128
|
+
});
|
|
25129
|
+
}
|
|
25130
|
+
/**
|
|
25131
|
+
* Convert canonical rulesync servers to Muse Code's native `mcp_servers` shape.
|
|
25132
|
+
* Each entry carries a `transport` discriminator: `stdio` servers spawn a
|
|
25133
|
+
* `command` (single string) with `args`/`env`, and `streamable_http` servers
|
|
25134
|
+
* are reached at a `url` with optional `headers`. Only documented fields are
|
|
25135
|
+
* emitted; a canonical `disabled: true` maps to Muse's `enabled: false`.
|
|
25136
|
+
*/
|
|
25137
|
+
function convertToMusecodeFormat(mcpServers, logger) {
|
|
25138
|
+
const result = {};
|
|
25139
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
25140
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
25141
|
+
if (!isRecord$1(config)) continue;
|
|
25142
|
+
if (declaresNoTransport(config)) {
|
|
25143
|
+
warnAndSkipMcpServer({
|
|
25144
|
+
toolName: "Muse Code",
|
|
25145
|
+
serverName: name,
|
|
25146
|
+
reason: "no transport",
|
|
25147
|
+
logger
|
|
25148
|
+
});
|
|
25149
|
+
continue;
|
|
25150
|
+
}
|
|
25151
|
+
const converted = {};
|
|
25152
|
+
if (isRemoteMcpServer(config)) {
|
|
25153
|
+
const url = resolveRemoteMcpUrl(config);
|
|
25154
|
+
if (!url) {
|
|
25155
|
+
warnAndSkipMcpServer({
|
|
25156
|
+
toolName: "Muse Code",
|
|
25157
|
+
serverName: name,
|
|
25158
|
+
reason: "a remote transport without a url",
|
|
25159
|
+
logger
|
|
25160
|
+
});
|
|
25161
|
+
continue;
|
|
25162
|
+
}
|
|
25163
|
+
const stated = config.type ?? config.transport;
|
|
25164
|
+
if (stated === "sse" || stated === "ws" || stated === void 0 && /^wss?:\/\//i.test(url)) {
|
|
25165
|
+
warnAndSkipMcpServer({
|
|
25166
|
+
toolName: "Muse Code",
|
|
25167
|
+
serverName: name,
|
|
25168
|
+
reason: `the "${stated ?? "ws"}" transport, which Muse Code does not implement (only stdio and streamable_http are supported)`,
|
|
25169
|
+
logger
|
|
25170
|
+
});
|
|
25171
|
+
continue;
|
|
25172
|
+
}
|
|
25173
|
+
converted.transport = "streamable_http";
|
|
25174
|
+
converted.url = url;
|
|
25175
|
+
if (config.headers && Object.keys(config.headers).length > 0) converted.headers = config.headers;
|
|
25176
|
+
} else {
|
|
25177
|
+
const [command, ...args] = resolveLocalMcpCommand(config);
|
|
25178
|
+
if (!command) {
|
|
25179
|
+
warnAndSkipMcpServer({
|
|
25180
|
+
toolName: "Muse Code",
|
|
25181
|
+
serverName: name,
|
|
25182
|
+
reason: "a stdio transport without a command",
|
|
25183
|
+
logger
|
|
25184
|
+
});
|
|
25185
|
+
continue;
|
|
25186
|
+
}
|
|
25187
|
+
converted.transport = "stdio";
|
|
25188
|
+
converted.command = command;
|
|
25189
|
+
converted.args = args;
|
|
25190
|
+
if (config.env && Object.keys(config.env).length > 0) converted.env = config.env;
|
|
25191
|
+
}
|
|
25192
|
+
if (config.disabled === true) converted.enabled = false;
|
|
25193
|
+
result[name] = converted;
|
|
25194
|
+
}
|
|
25195
|
+
return result;
|
|
25196
|
+
}
|
|
25197
|
+
/**
|
|
25198
|
+
* Convert Muse Code's native `mcp_servers` shape back to canonical rulesync
|
|
25199
|
+
* servers. The `transport` discriminator is dropped (`streamable_http` is not a
|
|
25200
|
+
* canonical enum value; the transport is re-derived from `command`/`url` on the
|
|
25201
|
+
* next generate), `enabled: false` maps back to `disabled: true`, and unknown
|
|
25202
|
+
* keys (e.g. `mode`, `framing`) pass through untouched.
|
|
25203
|
+
*/
|
|
25204
|
+
function convertFromMusecodeFormat(musecodeMcp) {
|
|
25205
|
+
const result = {};
|
|
25206
|
+
for (const [name, config] of Object.entries(musecodeMcp)) {
|
|
25207
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
25208
|
+
const converted = {};
|
|
25209
|
+
for (const [key, value] of Object.entries(config)) {
|
|
25210
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
25211
|
+
if (key === "transport") continue;
|
|
25212
|
+
if (key === "enabled") {
|
|
25213
|
+
if (value === false) converted.disabled = true;
|
|
25214
|
+
continue;
|
|
25215
|
+
}
|
|
25216
|
+
converted[key] = value;
|
|
25217
|
+
}
|
|
25218
|
+
result[name] = converted;
|
|
25219
|
+
}
|
|
25220
|
+
return result;
|
|
25221
|
+
}
|
|
25222
|
+
/**
|
|
25223
|
+
* Meta Muse Code MCP servers.
|
|
25224
|
+
*
|
|
25225
|
+
* Muse Code reads MCP servers only from the `mcp_servers` block of the GLOBAL
|
|
25226
|
+
* user settings file `~/.config/muse/settings.json`; no project-scoped MCP
|
|
25227
|
+
* location is documented. The settings file must carry
|
|
25228
|
+
* `"schema_version": 1` — a file that omits that key fails every command at
|
|
25229
|
+
* startup with `malformed settings file` — so the key is bootstrapped when the
|
|
25230
|
+
* file is created and preserved when it already exists. Other settings keys are
|
|
25231
|
+
* preserved via the shared-config gateway, and the file is never deleted.
|
|
25232
|
+
*
|
|
25233
|
+
* @see https://dev.meta.ai/docs/muse-code/configuration.md
|
|
25234
|
+
* @see https://dev.meta.ai/docs/muse-code/extending.md
|
|
25235
|
+
*/
|
|
25236
|
+
var MusecodeMcp = class MusecodeMcp extends ToolMcp {
|
|
25237
|
+
json;
|
|
25238
|
+
constructor(params) {
|
|
25239
|
+
super(params);
|
|
25240
|
+
this.json = parseMusecodeSettings(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
|
|
25241
|
+
}
|
|
25242
|
+
getJson() {
|
|
25243
|
+
return this.json;
|
|
25244
|
+
}
|
|
25245
|
+
isDeletable() {
|
|
25246
|
+
return false;
|
|
25247
|
+
}
|
|
25248
|
+
static getSettablePaths(_options) {
|
|
25249
|
+
return {
|
|
25250
|
+
relativeDirPath: MUSECODE_GLOBAL_CONFIG_DIR_PATH,
|
|
25251
|
+
relativeFilePath: MUSECODE_SETTINGS_FILE_NAME
|
|
25252
|
+
};
|
|
25253
|
+
}
|
|
25254
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
25255
|
+
if (!global) throw new Error(MUSECODE_GLOBAL_ONLY_MESSAGE);
|
|
25256
|
+
const paths = this.getSettablePaths({ global });
|
|
25257
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
|
|
25258
|
+
return new MusecodeMcp({
|
|
25259
|
+
outputRoot,
|
|
25260
|
+
relativeDirPath: paths.relativeDirPath,
|
|
25261
|
+
relativeFilePath: paths.relativeFilePath,
|
|
25262
|
+
fileContent,
|
|
25263
|
+
validate,
|
|
25264
|
+
global
|
|
25265
|
+
});
|
|
25266
|
+
}
|
|
25267
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
25268
|
+
if (!global) throw new Error(MUSECODE_GLOBAL_ONLY_MESSAGE);
|
|
25269
|
+
const paths = this.getSettablePaths({ global });
|
|
25270
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
25271
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
25272
|
+
const existing = parseMusecodeSettings(existingContent, filePath);
|
|
25273
|
+
const converted = convertToMusecodeFormat(rulesyncMcp.getMcpServers(), logger);
|
|
25274
|
+
return new MusecodeMcp({
|
|
25275
|
+
outputRoot,
|
|
25276
|
+
relativeDirPath: paths.relativeDirPath,
|
|
25277
|
+
relativeFilePath: paths.relativeFilePath,
|
|
25278
|
+
fileContent: applySharedConfigPatch({
|
|
25279
|
+
fileKey: sharedConfigFileKey(paths),
|
|
25280
|
+
feature: "mcp",
|
|
25281
|
+
existingContent,
|
|
25282
|
+
patch: {
|
|
25283
|
+
mcp_servers: converted,
|
|
25284
|
+
...existing.schema_version === void 0 && { schema_version: 1 }
|
|
25285
|
+
},
|
|
25286
|
+
filePath
|
|
25287
|
+
}),
|
|
25288
|
+
validate,
|
|
25289
|
+
global
|
|
25290
|
+
});
|
|
25291
|
+
}
|
|
25292
|
+
toRulesyncMcp() {
|
|
25293
|
+
const converted = convertFromMusecodeFormat(isRecord$1(this.json.mcp_servers) ? this.json.mcp_servers : {});
|
|
25294
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
|
|
25295
|
+
}
|
|
25296
|
+
validate() {
|
|
25297
|
+
return {
|
|
25298
|
+
success: true,
|
|
25299
|
+
error: null
|
|
25300
|
+
};
|
|
25301
|
+
}
|
|
25302
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
25303
|
+
return new MusecodeMcp({
|
|
25304
|
+
outputRoot,
|
|
25305
|
+
relativeDirPath,
|
|
25306
|
+
relativeFilePath,
|
|
25307
|
+
fileContent: JSON.stringify({ mcp_servers: {} }, null, 2),
|
|
25308
|
+
validate: false,
|
|
25309
|
+
global
|
|
25310
|
+
});
|
|
25311
|
+
}
|
|
25312
|
+
};
|
|
25313
|
+
//#endregion
|
|
24444
25314
|
//#region src/features/mcp/opencode-mcp.ts
|
|
24445
25315
|
const OPENCODE_ENV_VAR_PATTERN = /(?<!\$)\{env:([^}:]+)\}/g;
|
|
24446
25316
|
const OpencodeMcpLocalServerSchema = zod_mini.z.looseObject({
|
|
@@ -26397,6 +27267,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
26397
27267
|
supportsDisabledTools: false
|
|
26398
27268
|
}
|
|
26399
27269
|
}],
|
|
27270
|
+
["musecode", {
|
|
27271
|
+
class: MusecodeMcp,
|
|
27272
|
+
meta: {
|
|
27273
|
+
supportsProject: false,
|
|
27274
|
+
supportsGlobal: true,
|
|
27275
|
+
supportsEnabledTools: false,
|
|
27276
|
+
supportsDisabledTools: false
|
|
27277
|
+
}
|
|
27278
|
+
}],
|
|
26400
27279
|
["opencode", {
|
|
26401
27280
|
class: OpencodeMcp,
|
|
26402
27281
|
meta: {
|
|
@@ -29212,6 +30091,163 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
|
|
|
29212
30091
|
}
|
|
29213
30092
|
};
|
|
29214
30093
|
//#endregion
|
|
30094
|
+
//#region src/features/permissions/copilotcli-permissions.ts
|
|
30095
|
+
/**
|
|
30096
|
+
* The only canonical category the Copilot CLI settings can express: URL
|
|
30097
|
+
* approvals. Every other category (`bash`, `edit`, `read`, ...) has no
|
|
30098
|
+
* user-authorable surface — the CLI's `permissions.allow`/`ask`/`deny` rule
|
|
30099
|
+
* arrays are accepted only in MDM/enterprise managed settings, and session
|
|
30100
|
+
* tool approvals are machine-written to `permissions-config.json`.
|
|
30101
|
+
*/
|
|
30102
|
+
const WEBFETCH_CATEGORY = "webfetch";
|
|
30103
|
+
/** `~/.copilot/settings.json` and `.github/copilot/settings.json` keys. */
|
|
30104
|
+
const ALLOWED_URLS_KEY = "allowedUrls";
|
|
30105
|
+
const DENIED_URLS_KEY = "deniedUrls";
|
|
30106
|
+
function toUrlList(value) {
|
|
30107
|
+
if (!Array.isArray(value)) return [];
|
|
30108
|
+
return value.filter((entry) => typeof entry === "string");
|
|
30109
|
+
}
|
|
30110
|
+
/**
|
|
30111
|
+
* Split one canonical category's rules into the two URL lists. `ask` entries
|
|
30112
|
+
* are omitted: the CLI prompts for any URL that is in neither list, which is
|
|
30113
|
+
* exactly what `ask` means.
|
|
30114
|
+
*/
|
|
30115
|
+
function splitUrlRules(rules) {
|
|
30116
|
+
const allowedUrls = [];
|
|
30117
|
+
const deniedUrls = [];
|
|
30118
|
+
for (const [pattern, action] of Object.entries(rules)) if (action === "allow") allowedUrls.push(pattern);
|
|
30119
|
+
else if (action === "deny") deniedUrls.push(pattern);
|
|
30120
|
+
return {
|
|
30121
|
+
allowedUrls,
|
|
30122
|
+
deniedUrls
|
|
30123
|
+
};
|
|
30124
|
+
}
|
|
30125
|
+
/**
|
|
30126
|
+
* Permissions generator for the GitHub Copilot CLI.
|
|
30127
|
+
*
|
|
30128
|
+
* The CLI keeps two persistent settings files: the user-scope
|
|
30129
|
+
* `~/.copilot/settings.json` and the repository-scope
|
|
30130
|
+
* `.github/copilot/settings.json` (shipped in v1.0.60). Both are shared,
|
|
30131
|
+
* hand-edited files carrying unrelated keys (`model`, `effortLevel`, `hooks`,
|
|
30132
|
+
* `sandbox.*`, ...), so writes go through the shared-config gateway, only the
|
|
30133
|
+
* URL keys are owned, and the file is never deleted.
|
|
30134
|
+
*
|
|
30135
|
+
* The canonical `webfetch` category maps onto the CLI's two URL lists:
|
|
30136
|
+
* `allow` → `allowedUrls`, `deny` → `deniedUrls`, `ask` → the pattern is
|
|
30137
|
+
* omitted so the CLI falls through to its approval prompt.
|
|
30138
|
+
*
|
|
30139
|
+
* Scope asymmetry: the repository-scope key table documents `deniedUrls`
|
|
30140
|
+
* (union — a repository may add entries, never remove them) but NOT
|
|
30141
|
+
* `allowedUrls`, so an allow rule is only enforceable at user scope. At project
|
|
30142
|
+
* scope allow rules are therefore dropped with a warning rather than written to
|
|
30143
|
+
* a key the CLI ignores. (v1.0.79 additionally warns on startup about unknown
|
|
30144
|
+
* top-level keys in the user `settings.json`, so only documented keys are ever
|
|
30145
|
+
* emitted there either.)
|
|
30146
|
+
*
|
|
30147
|
+
* @see https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
|
|
30148
|
+
* @see https://github.com/github/copilot-cli/blob/main/changelog.md
|
|
30149
|
+
*/
|
|
30150
|
+
var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions {
|
|
30151
|
+
constructor(params) {
|
|
30152
|
+
super({
|
|
30153
|
+
...params,
|
|
30154
|
+
fileContent: params.fileContent ?? "{}"
|
|
30155
|
+
});
|
|
30156
|
+
}
|
|
30157
|
+
/**
|
|
30158
|
+
* `settings.json` holds unrelated user settings (`model`, `effortLevel`,
|
|
30159
|
+
* `hooks`, ...), so it must not be deleted.
|
|
30160
|
+
*/
|
|
30161
|
+
isDeletable() {
|
|
30162
|
+
return false;
|
|
30163
|
+
}
|
|
30164
|
+
static getSettablePaths({ global = false } = {}) {
|
|
30165
|
+
return global ? {
|
|
30166
|
+
relativeDirPath: COPILOT_DIR,
|
|
30167
|
+
relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
|
|
30168
|
+
} : {
|
|
30169
|
+
relativeDirPath: COPILOTCLI_PROJECT_SETTINGS_DIR_PATH,
|
|
30170
|
+
relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
|
|
30171
|
+
};
|
|
30172
|
+
}
|
|
30173
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
30174
|
+
const paths = CopilotcliPermissions.getSettablePaths({ global });
|
|
30175
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
|
|
30176
|
+
return new CopilotcliPermissions({
|
|
30177
|
+
outputRoot,
|
|
30178
|
+
relativeDirPath: paths.relativeDirPath,
|
|
30179
|
+
relativeFilePath: paths.relativeFilePath,
|
|
30180
|
+
fileContent,
|
|
30181
|
+
validate,
|
|
30182
|
+
global
|
|
30183
|
+
});
|
|
30184
|
+
}
|
|
30185
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
|
|
30186
|
+
const paths = CopilotcliPermissions.getSettablePaths({ global });
|
|
30187
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
30188
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
30189
|
+
const rules = rulesyncPermissions.getJson().permission[WEBFETCH_CATEGORY];
|
|
30190
|
+
const patch = {};
|
|
30191
|
+
if (rules !== void 0) {
|
|
30192
|
+
const { allowedUrls, deniedUrls } = splitUrlRules(rules);
|
|
30193
|
+
patch[DENIED_URLS_KEY] = deniedUrls.length > 0 ? deniedUrls : void 0;
|
|
30194
|
+
if (global) patch[ALLOWED_URLS_KEY] = allowedUrls.length > 0 ? allowedUrls : void 0;
|
|
30195
|
+
else if (allowedUrls.length > 0) logger?.warn(`Copilot CLI permissions: dropping ${allowedUrls.length} "webfetch" allow rule(s) at project scope — ${(0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)} does not accept "${ALLOWED_URLS_KEY}" (repository settings may only add denials). Author allow rules in global scope (\`--global\`) instead.`);
|
|
30196
|
+
}
|
|
30197
|
+
return new CopilotcliPermissions({
|
|
30198
|
+
outputRoot,
|
|
30199
|
+
relativeDirPath: paths.relativeDirPath,
|
|
30200
|
+
relativeFilePath: paths.relativeFilePath,
|
|
30201
|
+
fileContent: applySharedConfigPatch({
|
|
30202
|
+
fileKey: sharedConfigFileKey(paths),
|
|
30203
|
+
feature: "permissions",
|
|
30204
|
+
existingContent,
|
|
30205
|
+
patch,
|
|
30206
|
+
filePath
|
|
30207
|
+
}),
|
|
30208
|
+
validate: true,
|
|
30209
|
+
global
|
|
30210
|
+
});
|
|
30211
|
+
}
|
|
30212
|
+
toRulesyncPermissions() {
|
|
30213
|
+
const settings = this.parseSettings();
|
|
30214
|
+
const rules = {};
|
|
30215
|
+
for (const pattern of toUrlList(settings[DENIED_URLS_KEY])) rules[pattern] = "deny";
|
|
30216
|
+
if (this.global) for (const pattern of toUrlList(settings[ALLOWED_URLS_KEY])) rules[pattern] ??= "allow";
|
|
30217
|
+
const permission = Object.keys(rules).length > 0 ? { [WEBFETCH_CATEGORY]: rules } : {};
|
|
30218
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
|
|
30219
|
+
}
|
|
30220
|
+
parseSettings() {
|
|
30221
|
+
const relativePath = (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath());
|
|
30222
|
+
try {
|
|
30223
|
+
return parseSharedConfig({
|
|
30224
|
+
format: "json",
|
|
30225
|
+
fileContent: this.getFileContent() || "{}",
|
|
30226
|
+
filePath: relativePath,
|
|
30227
|
+
invalidRootPolicy: "error"
|
|
30228
|
+
});
|
|
30229
|
+
} catch (error) {
|
|
30230
|
+
throw new Error(`Failed to parse Copilot CLI settings in ${relativePath}: ${formatError(error)}`, { cause: error });
|
|
30231
|
+
}
|
|
30232
|
+
}
|
|
30233
|
+
validate() {
|
|
30234
|
+
return {
|
|
30235
|
+
success: true,
|
|
30236
|
+
error: null
|
|
30237
|
+
};
|
|
30238
|
+
}
|
|
30239
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
30240
|
+
return new CopilotcliPermissions({
|
|
30241
|
+
outputRoot,
|
|
30242
|
+
relativeDirPath,
|
|
30243
|
+
relativeFilePath,
|
|
30244
|
+
fileContent: "{}",
|
|
30245
|
+
validate: false,
|
|
30246
|
+
global
|
|
30247
|
+
});
|
|
30248
|
+
}
|
|
30249
|
+
};
|
|
30250
|
+
//#endregion
|
|
29215
30251
|
//#region src/features/permissions/cursor-permissions.ts
|
|
29216
30252
|
/**
|
|
29217
30253
|
* Mapping from rulesync canonical tool category names (lowercase) to Cursor CLI
|
|
@@ -30229,6 +31265,9 @@ const GROK_TOOL_TO_CATEGORY = {
|
|
|
30229
31265
|
WebSearch: "websearch"
|
|
30230
31266
|
};
|
|
30231
31267
|
const GROK_MCP_TOOL = "MCPTool";
|
|
31268
|
+
const GROK_TOOL_TO_CATEGORY_LOWER = Object.fromEntries(Object.entries(GROK_TOOL_TO_CATEGORY).map(([tool, category]) => [tool.toLowerCase(), category]));
|
|
31269
|
+
const GROK_MCP_TOOL_ALIASES = /* @__PURE__ */ new Set(["mcp", GROK_MCP_TOOL.toLowerCase()]);
|
|
31270
|
+
const GROKCLI_PERMISSION_RULES_KEY = "rules";
|
|
30232
31271
|
/**
|
|
30233
31272
|
* Build a Grok Claude-style permission entry (e.g. `Bash(git *)`, `Read`,
|
|
30234
31273
|
* `MCPTool(server__tool)`) from a canonical category + pattern. Returns `null`
|
|
@@ -30281,6 +31320,44 @@ function parseGrokEntry(entry) {
|
|
|
30281
31320
|
};
|
|
30282
31321
|
}
|
|
30283
31322
|
/**
|
|
31323
|
+
* Parse one entry of the verbose `[[permission.rules]]` form
|
|
31324
|
+
* (`{ action = "allow", tool = "bash", pattern = "git *" }`) into a canonical
|
|
31325
|
+
* category + pattern + action. Unlike the compact array entries these are TOML
|
|
31326
|
+
* tables, so the tool name arrives as its own field instead of an entry string.
|
|
31327
|
+
* Returns `null` for malformed entries and for tools with no canonical
|
|
31328
|
+
* equivalent, so callers can skip them (the whole `rules` array is preserved
|
|
31329
|
+
* verbatim on generate regardless).
|
|
31330
|
+
*/
|
|
31331
|
+
function parseGrokRule(rule) {
|
|
31332
|
+
if (!isRecord$1(rule)) return null;
|
|
31333
|
+
const { action: rawAction, tool, pattern } = rule;
|
|
31334
|
+
const parsedAction = PermissionActionSchema.safeParse(rawAction);
|
|
31335
|
+
if (!parsedAction.success) return null;
|
|
31336
|
+
const action = parsedAction.data;
|
|
31337
|
+
if (typeof tool !== "string") return null;
|
|
31338
|
+
if (pattern !== void 0 && typeof pattern !== "string") return null;
|
|
31339
|
+
const trimmedPattern = pattern?.trim() ?? "";
|
|
31340
|
+
const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$2;
|
|
31341
|
+
const lowerTool = tool.trim().toLowerCase();
|
|
31342
|
+
if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$2 ? {
|
|
31343
|
+
category: "mcp",
|
|
31344
|
+
pattern: CATCH_ALL_PATTERN$2,
|
|
31345
|
+
action
|
|
31346
|
+
} : {
|
|
31347
|
+
category: `${MCP_CANONICAL_PREFIX$1}${resolvedPattern}`,
|
|
31348
|
+
pattern: CATCH_ALL_PATTERN$2,
|
|
31349
|
+
action
|
|
31350
|
+
};
|
|
31351
|
+
if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
|
|
31352
|
+
const category = GROK_TOOL_TO_CATEGORY_LOWER[lowerTool];
|
|
31353
|
+
if (category === void 0) return null;
|
|
31354
|
+
return {
|
|
31355
|
+
category,
|
|
31356
|
+
pattern: resolvedPattern,
|
|
31357
|
+
action
|
|
31358
|
+
};
|
|
31359
|
+
}
|
|
31360
|
+
/**
|
|
30284
31361
|
* Permissions adapter for the xAI Grok Build CLI (`grokcli`).
|
|
30285
31362
|
*
|
|
30286
31363
|
* Grok Build CLI ships a Claude-style rule system under `[permission]` in
|
|
@@ -30301,9 +31378,17 @@ function parseGrokEntry(entry) {
|
|
|
30301
31378
|
* entry with different actions (e.g. `edit` allow + `write` deny → `Edit`),
|
|
30302
31379
|
* the strictest wins (`deny > ask > allow`) and a warning is logged, so the
|
|
30303
31380
|
* entry never lands contradictorily in two arrays.
|
|
30304
|
-
* - Import:
|
|
30305
|
-
* categories
|
|
30306
|
-
*
|
|
31381
|
+
* - Import: both documented `[permission]` forms are parsed back into
|
|
31382
|
+
* canonical categories — the compact `allow`/`deny`/`ask` arrays and the
|
|
31383
|
+
* verbose `[[permission.rules]]` tables
|
|
31384
|
+
* (`{ action = "allow", tool = "bash", pattern = "git *" }`), whose `tool`
|
|
31385
|
+
* field is matched case-insensitively against the same tool table, plus
|
|
31386
|
+
* the documented `mcp` alias for the compact form's `MCPTool`. Rules
|
|
31387
|
+
* from the two forms are merged with the same `deny > ask > allow`
|
|
31388
|
+
* precedence. When neither form carries a rule (older configs), the coarse
|
|
31389
|
+
* `[ui] permission_mode` is used as a fallback. Generate always emits the
|
|
31390
|
+
* compact arrays, so a user-authored `rules` array is preserved verbatim
|
|
31391
|
+
* but not reconciled against them.
|
|
30307
31392
|
*
|
|
30308
31393
|
* The coarse `[ui] permission_mode` toggle is still written for backward
|
|
30309
31394
|
* compatibility with older Grok versions: `always-approve` when the config is
|
|
@@ -30470,29 +31555,44 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
|
|
|
30470
31555
|
};
|
|
30471
31556
|
}
|
|
30472
31557
|
/**
|
|
30473
|
-
* Parse Grok's `[permission]`
|
|
30474
|
-
*
|
|
30475
|
-
*
|
|
30476
|
-
*
|
|
30477
|
-
*
|
|
31558
|
+
* Parse Grok's `[permission]` section back into a canonical permission map,
|
|
31559
|
+
* reading both supported forms: the compact `allow`/`deny`/`ask` arrays of
|
|
31560
|
+
* Claude-style entries and the verbose `[[permission.rules]]` tables
|
|
31561
|
+
* (`{ action, tool, pattern }`). Returns `null` only when neither form carries
|
|
31562
|
+
* any rule, so the caller can fall back to the coarse `permission_mode`.
|
|
31563
|
+
* Precedence `deny > ask > allow` is applied across both forms, so a tool
|
|
31564
|
+
* described more than once resolves to the strictest action.
|
|
30478
31565
|
*/
|
|
30479
31566
|
function parseGrokPermissionArrays(permission) {
|
|
30480
31567
|
const allow = isStringArray$1(permission.allow) ? permission.allow : void 0;
|
|
30481
31568
|
const deny = isStringArray$1(permission.deny) ? permission.deny : void 0;
|
|
30482
31569
|
const ask = isStringArray$1(permission.ask) ? permission.ask : void 0;
|
|
30483
|
-
|
|
31570
|
+
const rules = Array.isArray(permission[GROKCLI_PERMISSION_RULES_KEY]) ? permission[GROKCLI_PERMISSION_RULES_KEY] : void 0;
|
|
31571
|
+
if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) + (rules?.length ?? 0) === 0) return null;
|
|
30484
31572
|
const result = {};
|
|
30485
|
-
const
|
|
31573
|
+
const record = ({ category, pattern, action }) => {
|
|
31574
|
+
const bucket = result[category] ??= {};
|
|
31575
|
+
const existing = bucket[pattern];
|
|
31576
|
+
if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) bucket[pattern] = action;
|
|
31577
|
+
};
|
|
31578
|
+
const applyEntries = (entries, action) => {
|
|
30486
31579
|
for (const entry of entries ?? []) {
|
|
30487
31580
|
const parsed = parseGrokEntry(entry);
|
|
30488
31581
|
if (parsed === null) continue;
|
|
30489
|
-
|
|
30490
|
-
|
|
31582
|
+
record({
|
|
31583
|
+
...parsed,
|
|
31584
|
+
action
|
|
31585
|
+
});
|
|
30491
31586
|
}
|
|
30492
31587
|
};
|
|
30493
|
-
|
|
30494
|
-
|
|
30495
|
-
|
|
31588
|
+
applyEntries(allow, "allow");
|
|
31589
|
+
applyEntries(ask, "ask");
|
|
31590
|
+
applyEntries(deny, "deny");
|
|
31591
|
+
for (const rule of rules ?? []) {
|
|
31592
|
+
const parsed = parseGrokRule(rule);
|
|
31593
|
+
if (parsed === null) continue;
|
|
31594
|
+
record(parsed);
|
|
31595
|
+
}
|
|
30496
31596
|
return result;
|
|
30497
31597
|
}
|
|
30498
31598
|
/**
|
|
@@ -33177,6 +34277,13 @@ const TAKT_PROVIDER_PROFILES_KEY = "provider_profiles";
|
|
|
33177
34277
|
const TAKT_DEFAULT_PERMISSION_MODE_KEY = "default_permission_mode";
|
|
33178
34278
|
const TAKT_STEP_PERMISSION_OVERRIDES_KEY = "step_permission_overrides";
|
|
33179
34279
|
const TAKT_PROVIDER_OPTIONS_KEY = "provider_options";
|
|
34280
|
+
const TAKT_RUNTIME_PROVIDER_KEY = "provider";
|
|
34281
|
+
const TAKT_RUNTIME_DEFAULTS_KEY = "defaults";
|
|
34282
|
+
const TAKT_RUNTIME_PROFILES_KEY = "profiles";
|
|
34283
|
+
const TAKT_RUNTIME_TARGETS_KEY = "targets";
|
|
34284
|
+
const TAKT_RUNTIME_AUTO_ROUTING_KEY = "auto_routing";
|
|
34285
|
+
const TAKT_RUNTIME_PROFILE_KEY = "profile";
|
|
34286
|
+
const TAKT_RUNTIME_OPTIONS_KEY = "options";
|
|
33180
34287
|
const TAKT_SECURITY_POLICIES = {
|
|
33181
34288
|
workflow_arpeggio: [
|
|
33182
34289
|
"custom_data_source_modules",
|
|
@@ -33229,13 +34336,36 @@ const CATCH_ALL_PATTERN = "*";
|
|
|
33229
34336
|
* profile, layered on top of `default_permission_mode`) and `provider_options`
|
|
33230
34337
|
* (a top-level per-provider sandbox/network table). Both are authored on
|
|
33231
34338
|
* generate and re-extracted on import.
|
|
34339
|
+
*
|
|
34340
|
+
* Takt 0.56.0 moved provider configuration into a separate `runtime.yaml`
|
|
34341
|
+
* (`.takt/runtime.yaml`, `~/.takt/runtime.yaml`). Once its `provider:` section
|
|
34342
|
+
* carries an assignment ("runtime mode" — the state a freshly installed Takt is
|
|
34343
|
+
* in, since it generates an active global runtime.yaml on first launch), any
|
|
34344
|
+
* legacy provider setting left in `config.yaml` makes Takt stop before running
|
|
34345
|
+
* an agent with "Mixed provider configuration detected". rulesync only reads
|
|
34346
|
+
* that file, in two places:
|
|
34347
|
+
* - the active provider is resolved from it first (the `provider_profiles`
|
|
34348
|
+
* key that carries the permission mode is still keyed by provider name, and
|
|
34349
|
+
* `provider_profiles` is not itself a legacy signal);
|
|
34350
|
+
* - an authored `provider_options` is refused with a warning rather than
|
|
34351
|
+
* written while runtime mode is active, and its runtime-side counterpart
|
|
34352
|
+
* (`provider.profiles.*.options`) is lifted back out on import.
|
|
34353
|
+
* Installs with no runtime.yaml, or an inactive one, keep the legacy behavior
|
|
34354
|
+
* unchanged.
|
|
33232
34355
|
*/
|
|
33233
34356
|
var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
33234
|
-
|
|
34357
|
+
/**
|
|
34358
|
+
* The sibling `runtime.yaml` as read from the same scope, or `""` when the
|
|
34359
|
+
* install has none. Import needs it — `toRulesyncPermissions()` is
|
|
34360
|
+
* synchronous, so the file is read alongside config.yaml in `fromFile()`.
|
|
34361
|
+
*/
|
|
34362
|
+
runtimeFileContent;
|
|
34363
|
+
constructor({ runtimeFileContent, ...params }) {
|
|
33235
34364
|
super({
|
|
33236
34365
|
...params,
|
|
33237
34366
|
fileContent: params.fileContent ?? ""
|
|
33238
34367
|
});
|
|
34368
|
+
this.runtimeFileContent = runtimeFileContent ?? "";
|
|
33239
34369
|
}
|
|
33240
34370
|
isDeletable() {
|
|
33241
34371
|
return false;
|
|
@@ -33249,11 +34379,13 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
|
33249
34379
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
33250
34380
|
const paths = TaktPermissions.getSettablePaths({ global });
|
|
33251
34381
|
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
34382
|
+
const runtimeFileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, "runtime.yaml")) ?? "";
|
|
33252
34383
|
return new TaktPermissions({
|
|
33253
34384
|
outputRoot,
|
|
33254
34385
|
relativeDirPath: paths.relativeDirPath,
|
|
33255
34386
|
relativeFilePath: paths.relativeFilePath,
|
|
33256
34387
|
fileContent,
|
|
34388
|
+
runtimeFileContent,
|
|
33257
34389
|
validate,
|
|
33258
34390
|
global
|
|
33259
34391
|
});
|
|
@@ -33268,12 +34400,22 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
|
33268
34400
|
filePath,
|
|
33269
34401
|
invalidRootPolicy: "error"
|
|
33270
34402
|
});
|
|
34403
|
+
const runtime = await readTaktRuntimeConfig({
|
|
34404
|
+
outputRoot,
|
|
34405
|
+
logger
|
|
34406
|
+
});
|
|
33271
34407
|
const rulesyncJson = rulesyncPermissions.getJson();
|
|
33272
|
-
const provider = resolveActiveProvider(
|
|
34408
|
+
const provider = resolveActiveProvider({
|
|
34409
|
+
config,
|
|
34410
|
+
runtime: runtime.active ? runtime.config : void 0
|
|
34411
|
+
});
|
|
33273
34412
|
const mode = deriveTaktPermissionMode(rulesyncJson);
|
|
33274
34413
|
const override = isPlainObject$1(rulesyncJson.takt) ? rulesyncJson.takt : void 0;
|
|
33275
34414
|
const stepOverrides = isPlainObject$1(override?.[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? override[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
|
|
33276
|
-
const
|
|
34415
|
+
const authoredProviderOptions = isPlainObject$1(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
|
|
34416
|
+
const runtimeModeRefusesProviderOptions = authoredProviderOptions !== void 0 && runtime.active;
|
|
34417
|
+
if (runtimeModeRefusesProviderOptions) logger?.warn(`Takt permissions: not writing "${TAKT_PROVIDER_OPTIONS_KEY}" to ${filePath} because ${runtime.filePaths.join(" + ")} puts Takt in runtime provider mode (Takt 0.56.0+), where any legacy provider setting in config.yaml makes Takt fail with "Mixed provider configuration detected". Move those options to \`provider.profiles.<profile>.options\` in runtime.yaml, and remove them from the \`takt\` block of the rulesync source. A "${TAKT_PROVIDER_OPTIONS_KEY}" already in ${filePath} is left untouched — rulesync does not own that key.`);
|
|
34418
|
+
const overrideProviderOptions = runtimeModeRefusesProviderOptions ? void 0 : authoredProviderOptions;
|
|
33277
34419
|
const authoredPolicies = pickSecurityPolicies(override, {
|
|
33278
34420
|
filePath,
|
|
33279
34421
|
logger,
|
|
@@ -33319,13 +34461,26 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
|
33319
34461
|
filePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath()),
|
|
33320
34462
|
invalidRootPolicy: "error"
|
|
33321
34463
|
});
|
|
33322
|
-
const
|
|
34464
|
+
const runtimeFilePath = (0, node_path.join)(this.getRelativeDirPath(), TAKT_RUNTIME_CONFIG_FILE_NAME);
|
|
34465
|
+
const runtime = parseSharedConfig({
|
|
34466
|
+
format: "yaml",
|
|
34467
|
+
fileContent: this.runtimeFileContent,
|
|
34468
|
+
filePath: runtimeFilePath
|
|
34469
|
+
});
|
|
34470
|
+
const runtimeActive = isRuntimeModeActive(runtime);
|
|
34471
|
+
const provider = resolveActiveProvider({
|
|
34472
|
+
config,
|
|
34473
|
+
runtime: runtimeActive ? runtime : void 0
|
|
34474
|
+
});
|
|
33323
34475
|
const profiles = isPlainObject$1(config[TAKT_PROVIDER_PROFILES_KEY]) ? config[TAKT_PROVIDER_PROFILES_KEY] : {};
|
|
33324
34476
|
const profile = isPlainObject$1(profiles[provider]) ? profiles[provider] : {};
|
|
33325
34477
|
const mode = profile[TAKT_DEFAULT_PERMISSION_MODE_KEY];
|
|
33326
34478
|
const rulesyncConfig = taktModeToRulesyncConfig(mode);
|
|
33327
34479
|
const stepOverrides = isPlainObject$1(profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
|
|
33328
|
-
const providerOptions =
|
|
34480
|
+
const providerOptions = mergeSharedConfigDeep({
|
|
34481
|
+
base: isPlainObject$1(config[TAKT_PROVIDER_OPTIONS_KEY]) ? config[TAKT_PROVIDER_OPTIONS_KEY] : {},
|
|
34482
|
+
patch: runtimeActive ? collectRuntimeProviderOptions(runtime) : {}
|
|
34483
|
+
});
|
|
33329
34484
|
const taktOverride = {};
|
|
33330
34485
|
if (stepOverrides && Object.keys(stepOverrides).length > 0) taktOverride[TAKT_STEP_PERMISSION_OVERRIDES_KEY] = stepOverrides;
|
|
33331
34486
|
if (providerOptions && Object.keys(providerOptions).length > 0) taktOverride[TAKT_PROVIDER_OPTIONS_KEY] = providerOptions;
|
|
@@ -33352,10 +34507,18 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
|
33352
34507
|
}
|
|
33353
34508
|
};
|
|
33354
34509
|
/**
|
|
33355
|
-
* Resolve the active Takt provider
|
|
33356
|
-
*
|
|
34510
|
+
* Resolve the active Takt provider.
|
|
34511
|
+
*
|
|
34512
|
+
* Under runtime mode (Takt 0.56.0+) the real provider assignment lives in
|
|
34513
|
+
* `runtime.yaml`, and `config.yaml:provider` cannot coexist with it, so the
|
|
34514
|
+
* runtime document is consulted first: the provider of the profile named by
|
|
34515
|
+
* `provider.defaults.profile`. Everything else falls through to the legacy
|
|
34516
|
+
* chain — the top-level `provider:` value, else the sole key in
|
|
34517
|
+
* `provider_profiles`, else the `claude` default.
|
|
33357
34518
|
*/
|
|
33358
|
-
function resolveActiveProvider(config) {
|
|
34519
|
+
function resolveActiveProvider({ config, runtime }) {
|
|
34520
|
+
const fromRuntime = runtime === void 0 ? void 0 : resolveRuntimeProvider(runtime);
|
|
34521
|
+
if (fromRuntime !== void 0) return fromRuntime;
|
|
33359
34522
|
if (typeof config[TAKT_PROVIDER_KEY] === "string" && config[TAKT_PROVIDER_KEY].trim() !== "") return config[TAKT_PROVIDER_KEY];
|
|
33360
34523
|
const profiles = config[TAKT_PROVIDER_PROFILES_KEY];
|
|
33361
34524
|
if (isPlainObject$1(profiles)) {
|
|
@@ -33364,6 +34527,170 @@ function resolveActiveProvider(config) {
|
|
|
33364
34527
|
}
|
|
33365
34528
|
return TAKT_DEFAULT_PROVIDER;
|
|
33366
34529
|
}
|
|
34530
|
+
/** A plain object carrying at least one entry (Takt's "assignment" test). */
|
|
34531
|
+
function hasEntries(value) {
|
|
34532
|
+
return isPlainObject$1(value) && Object.keys(value).length > 0;
|
|
34533
|
+
}
|
|
34534
|
+
function runtimeProfiles(runtime) {
|
|
34535
|
+
const provider = runtime[TAKT_RUNTIME_PROVIDER_KEY];
|
|
34536
|
+
if (!isPlainObject$1(provider)) return {};
|
|
34537
|
+
const profiles = provider[TAKT_RUNTIME_PROFILES_KEY];
|
|
34538
|
+
return isPlainObject$1(profiles) ? profiles : {};
|
|
34539
|
+
}
|
|
34540
|
+
/**
|
|
34541
|
+
* Whether a parsed `runtime.yaml` puts Takt into runtime mode, mirroring
|
|
34542
|
+
* upstream's mode detection: the `provider:` section must carry an actual
|
|
34543
|
+
* assignment — a non-empty `defaults`, `profiles` or `auto_routing`, or a
|
|
34544
|
+
* `targets` map with at least one non-empty nested map. The file existing is not
|
|
34545
|
+
* enough, and empty nested maps (`defaults: {}`, `targets: { personas: {} }`)
|
|
34546
|
+
* must not flip the mode.
|
|
34547
|
+
* https://github.com/nrslib/takt/blob/main/src/infra/config/runtime-provider/mode.ts
|
|
34548
|
+
*/
|
|
34549
|
+
function isRuntimeModeActive(runtime) {
|
|
34550
|
+
const provider = runtime[TAKT_RUNTIME_PROVIDER_KEY];
|
|
34551
|
+
if (!isPlainObject$1(provider)) return false;
|
|
34552
|
+
if (hasEntries(provider[TAKT_RUNTIME_DEFAULTS_KEY]) || hasEntries(provider[TAKT_RUNTIME_PROFILES_KEY]) || hasEntries(provider[TAKT_RUNTIME_AUTO_ROUTING_KEY])) return true;
|
|
34553
|
+
const targets = provider[TAKT_RUNTIME_TARGETS_KEY];
|
|
34554
|
+
return isPlainObject$1(targets) && Object.values(targets).some(hasEntries);
|
|
34555
|
+
}
|
|
34556
|
+
/** The provider named by the runtime profile Takt would use by default. */
|
|
34557
|
+
function resolveRuntimeProvider(runtime) {
|
|
34558
|
+
const provider = runtime[TAKT_RUNTIME_PROVIDER_KEY];
|
|
34559
|
+
const profiles = runtimeProfiles(runtime);
|
|
34560
|
+
const providerOf = (profileName) => {
|
|
34561
|
+
if (profileName === void 0) return;
|
|
34562
|
+
const profile = profiles[profileName];
|
|
34563
|
+
if (!isPlainObject$1(profile)) return;
|
|
34564
|
+
const value = profile[TAKT_RUNTIME_PROVIDER_KEY];
|
|
34565
|
+
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
34566
|
+
};
|
|
34567
|
+
if (!isPlainObject$1(provider)) return;
|
|
34568
|
+
const defaults = provider[TAKT_RUNTIME_DEFAULTS_KEY];
|
|
34569
|
+
if (!isPlainObject$1(defaults) || typeof defaults[TAKT_RUNTIME_PROFILE_KEY] !== "string") return;
|
|
34570
|
+
return providerOf(defaults[TAKT_RUNTIME_PROFILE_KEY]);
|
|
34571
|
+
}
|
|
34572
|
+
/**
|
|
34573
|
+
* Lift the runtime profiles' flat `options` bags back into the per-provider
|
|
34574
|
+
* shape the `takt` override uses. Each profile's options belong to that
|
|
34575
|
+
* profile's own provider; when several profiles name the same provider they are
|
|
34576
|
+
* merged in document order, so a later profile wins on a colliding option key.
|
|
34577
|
+
*/
|
|
34578
|
+
function collectRuntimeProviderOptions(runtime) {
|
|
34579
|
+
const collected = {};
|
|
34580
|
+
for (const profile of Object.values(runtimeProfiles(runtime))) {
|
|
34581
|
+
if (!isPlainObject$1(profile)) continue;
|
|
34582
|
+
const providerName = profile[TAKT_RUNTIME_PROVIDER_KEY];
|
|
34583
|
+
const options = profile[TAKT_RUNTIME_OPTIONS_KEY];
|
|
34584
|
+
if (typeof providerName !== "string" || providerName.trim() === "" || !hasEntries(options)) continue;
|
|
34585
|
+
const existing = collected[providerName];
|
|
34586
|
+
collected[providerName] = {
|
|
34587
|
+
...isPlainObject$1(existing) ? existing : {},
|
|
34588
|
+
...options
|
|
34589
|
+
};
|
|
34590
|
+
}
|
|
34591
|
+
return collected;
|
|
34592
|
+
}
|
|
34593
|
+
/**
|
|
34594
|
+
* Read one `runtime.yaml`. A file that cannot be parsed reports `unparsable`
|
|
34595
|
+
* rather than a document: Takt refuses to start on a broken runtime.yaml, and
|
|
34596
|
+
* treating it as legacy would be the one outcome that writes the
|
|
34597
|
+
* mixed-configuration key into the user's config.yaml.
|
|
34598
|
+
*/
|
|
34599
|
+
async function readTaktRuntimeFile({ filePath, logger }) {
|
|
34600
|
+
const fileContent = await readFileContentOrNull(filePath);
|
|
34601
|
+
if (fileContent === null) return;
|
|
34602
|
+
try {
|
|
34603
|
+
return {
|
|
34604
|
+
config: parseSharedConfig({
|
|
34605
|
+
format: "yaml",
|
|
34606
|
+
fileContent,
|
|
34607
|
+
filePath
|
|
34608
|
+
}),
|
|
34609
|
+
unparsable: false
|
|
34610
|
+
};
|
|
34611
|
+
} catch (error) {
|
|
34612
|
+
logger?.warn(`Takt permissions: could not parse ${filePath} (${formatError(error)}); assuming Takt's runtime provider mode is active so no legacy provider setting is written to config.yaml.`);
|
|
34613
|
+
return {
|
|
34614
|
+
config: {},
|
|
34615
|
+
unparsable: true
|
|
34616
|
+
};
|
|
34617
|
+
}
|
|
34618
|
+
}
|
|
34619
|
+
/**
|
|
34620
|
+
* Collapse the project and global `runtime.yaml` into the single document Takt
|
|
34621
|
+
* itself resolves against, matching upstream's loader: `profiles` is a union
|
|
34622
|
+
* with the project definition of a same-named profile replacing the global one,
|
|
34623
|
+
* while `defaults`, `targets` and `auto_routing` are taken from the project file
|
|
34624
|
+
* whole whenever it states them at all (`??`, so a project `targets: {}` masks
|
|
34625
|
+
* the global one rather than merging with it).
|
|
34626
|
+
* https://github.com/nrslib/takt/blob/main/src/infra/config/runtime-provider/loader.ts
|
|
34627
|
+
*
|
|
34628
|
+
* Merging before mode detection matters in both directions: a project file
|
|
34629
|
+
* active only through `targets:` still resolves its provider from the global
|
|
34630
|
+
* file's `defaults`/`profiles`, and a project section that masks the global one
|
|
34631
|
+
* leaves the merged document inactive even though the global file alone was not.
|
|
34632
|
+
*/
|
|
34633
|
+
function mergeTaktRuntimeConfigs({ project, global: globalConfig }) {
|
|
34634
|
+
const sectionOf = (config) => {
|
|
34635
|
+
const provider = config?.[TAKT_RUNTIME_PROVIDER_KEY];
|
|
34636
|
+
return isPlainObject$1(provider) ? provider : {};
|
|
34637
|
+
};
|
|
34638
|
+
const projectSection = sectionOf(project);
|
|
34639
|
+
const globalSection = sectionOf(globalConfig);
|
|
34640
|
+
const replaced = (key) => projectSection[key] ?? globalSection[key];
|
|
34641
|
+
const profilesOf = (section) => isPlainObject$1(section[TAKT_RUNTIME_PROFILES_KEY]) ? section[TAKT_RUNTIME_PROFILES_KEY] : void 0;
|
|
34642
|
+
const globalProfiles = profilesOf(globalSection);
|
|
34643
|
+
const projectProfiles = profilesOf(projectSection);
|
|
34644
|
+
const provider = {};
|
|
34645
|
+
if (globalProfiles !== void 0 || projectProfiles !== void 0) provider[TAKT_RUNTIME_PROFILES_KEY] = {
|
|
34646
|
+
...globalProfiles,
|
|
34647
|
+
...projectProfiles
|
|
34648
|
+
};
|
|
34649
|
+
for (const key of [
|
|
34650
|
+
TAKT_RUNTIME_DEFAULTS_KEY,
|
|
34651
|
+
TAKT_RUNTIME_TARGETS_KEY,
|
|
34652
|
+
TAKT_RUNTIME_AUTO_ROUTING_KEY
|
|
34653
|
+
]) {
|
|
34654
|
+
const value = replaced(key);
|
|
34655
|
+
if (value !== void 0) provider[key] = value;
|
|
34656
|
+
}
|
|
34657
|
+
return { [TAKT_RUNTIME_PROVIDER_KEY]: provider };
|
|
34658
|
+
}
|
|
34659
|
+
/**
|
|
34660
|
+
* The runtime provider configuration in force for a generate run: the project
|
|
34661
|
+
* and global `runtime.yaml` merged the way Takt merges them, plus whether the
|
|
34662
|
+
* result puts Takt in runtime mode.
|
|
34663
|
+
*
|
|
34664
|
+
* Both scopes are read whichever scope is being generated: Takt collects legacy
|
|
34665
|
+
* signals from the project and global `config.yaml` alike, so a global
|
|
34666
|
+
* `runtime.yaml` — the one Takt generates on first launch — makes a legacy key
|
|
34667
|
+
* in the project config.yaml a hard failure just the same.
|
|
34668
|
+
*/
|
|
34669
|
+
async function readTaktRuntimeConfig({ outputRoot, logger }) {
|
|
34670
|
+
const projectPath = (0, node_path.join)(outputRoot, TAKT_DIR, TAKT_RUNTIME_CONFIG_FILE_NAME);
|
|
34671
|
+
let globalPath;
|
|
34672
|
+
try {
|
|
34673
|
+
const resolved = (0, node_path.join)(getHomeDirectory(), TAKT_DIR, TAKT_RUNTIME_CONFIG_FILE_NAME);
|
|
34674
|
+
globalPath = resolved === projectPath ? void 0 : resolved;
|
|
34675
|
+
} catch {}
|
|
34676
|
+
const [project, globalFile] = await Promise.all([readTaktRuntimeFile({
|
|
34677
|
+
filePath: projectPath,
|
|
34678
|
+
logger
|
|
34679
|
+
}), globalPath === void 0 ? Promise.resolve(void 0) : readTaktRuntimeFile({
|
|
34680
|
+
filePath: globalPath,
|
|
34681
|
+
logger
|
|
34682
|
+
})]);
|
|
34683
|
+
const filePaths = [...project === void 0 ? [] : [projectPath], ...globalFile === void 0 || globalPath === void 0 ? [] : [globalPath]];
|
|
34684
|
+
const config = mergeTaktRuntimeConfigs({
|
|
34685
|
+
project: project?.config,
|
|
34686
|
+
global: globalFile?.config
|
|
34687
|
+
});
|
|
34688
|
+
return {
|
|
34689
|
+
filePaths,
|
|
34690
|
+
config,
|
|
34691
|
+
active: project?.unparsable === true || globalFile?.unparsable === true || isRuntimeModeActive(config)
|
|
34692
|
+
};
|
|
34693
|
+
}
|
|
33367
34694
|
/**
|
|
33368
34695
|
* Collapse a rulesync permissions config into Takt's single coarse mode.
|
|
33369
34696
|
*
|
|
@@ -34223,6 +35550,57 @@ function asRecord(value) {
|
|
|
34223
35550
|
return Object.fromEntries(Object.entries(value));
|
|
34224
35551
|
}
|
|
34225
35552
|
/**
|
|
35553
|
+
* The `agent` keys the `zed` override may author. Everything else — above all
|
|
35554
|
+
* `tool_permissions`, which the canonical `permission` block owns end to end —
|
|
35555
|
+
* is refused, so an override can never reach past its own surface and weaken a
|
|
35556
|
+
* canonical deny. The override object is read key by key rather than spread, so
|
|
35557
|
+
* an unlisted key is inert whether or not it is named here — an allowlist, not a
|
|
35558
|
+
* denylist, because `agent` carries blunt instruments of its own (Zed's
|
|
35559
|
+
* `always_allow_tool_actions` would disarm every permission rule at once), and a
|
|
35560
|
+
* verbatim `agent` merge would hand them to the override.
|
|
35561
|
+
*
|
|
35562
|
+
* Every key the patch does not consume is reported, so an unsupported or
|
|
35563
|
+
* misspelled one surfaces as a warning rather than as config that quietly does
|
|
35564
|
+
* nothing. `permission` is exempt: it is the canonical tool-scoped block, and
|
|
35565
|
+
* `RulesyncPermissions.forTarget` has already consumed and stripped it.
|
|
35566
|
+
*/
|
|
35567
|
+
const ZED_OVERRIDE_AGENT_KEYS = ["sandbox_permissions", "profiles"];
|
|
35568
|
+
const ZED_CANONICAL_AGENT_KEY = "tool_permissions";
|
|
35569
|
+
const ZED_OVERRIDE_CONSUMED_KEYS = /* @__PURE__ */ new Set([...ZED_OVERRIDE_AGENT_KEYS, "permission"]);
|
|
35570
|
+
/**
|
|
35571
|
+
* Build the `agent` patch fragment carrying the `zed` override's verbatim
|
|
35572
|
+
* blocks. A key the override supplies replaces the existing block wholesale
|
|
35573
|
+
* (Zed reads each as one unit — a deep merge would leave half of a rewritten
|
|
35574
|
+
* sandbox policy behind); a key it omits is left out of the fragment, so the
|
|
35575
|
+
* existing value survives via the caller's spread of `agent`.
|
|
35576
|
+
*/
|
|
35577
|
+
function buildZedOverridePatch({ override, logger }) {
|
|
35578
|
+
if (!isPlainObject$1(override)) return {};
|
|
35579
|
+
if (ZED_CANONICAL_AGENT_KEY in override) logger?.warn(`Zed permissions: ignoring the 'zed.${ZED_CANONICAL_AGENT_KEY}' override; \`agent.${ZED_CANONICAL_AGENT_KEY}\` is driven by the canonical permission block.`);
|
|
35580
|
+
const unsupportedKeys = Object.keys(override).filter((key) => key !== ZED_CANONICAL_AGENT_KEY && !ZED_OVERRIDE_CONSUMED_KEYS.has(key));
|
|
35581
|
+
if (unsupportedKeys.length > 0) logger?.warn(`Zed permissions: ignoring the ${unsupportedKeys.map((key) => `'zed.${key}'`).join(", ")} override ${unsupportedKeys.length === 1 ? "key" : "keys"} — the \`zed\` block authors only ${ZED_OVERRIDE_AGENT_KEYS.map((key) => `\`${key}\``).join(" and ")}.`);
|
|
35582
|
+
const patch = {};
|
|
35583
|
+
for (const key of ZED_OVERRIDE_AGENT_KEYS) {
|
|
35584
|
+
const value = override[key];
|
|
35585
|
+
if (isPlainObject$1(value)) patch[key] = value;
|
|
35586
|
+
}
|
|
35587
|
+
return patch;
|
|
35588
|
+
}
|
|
35589
|
+
/**
|
|
35590
|
+
* The write-side inverse: lift `agent.sandbox_permissions` / `agent.profiles`
|
|
35591
|
+
* back into the `zed` override so a hand-written sandbox policy or profile set
|
|
35592
|
+
* round-trips instead of being lost on the next generate. Returns `undefined`
|
|
35593
|
+
* when the settings carry neither, so the override key is omitted.
|
|
35594
|
+
*/
|
|
35595
|
+
function extractZedOverride(agent) {
|
|
35596
|
+
const override = {};
|
|
35597
|
+
for (const key of ZED_OVERRIDE_AGENT_KEYS) {
|
|
35598
|
+
const value = agent[key];
|
|
35599
|
+
if (isPlainObject$1(value)) override[key] = value;
|
|
35600
|
+
}
|
|
35601
|
+
return Object.keys(override).length > 0 ? override : void 0;
|
|
35602
|
+
}
|
|
35603
|
+
/**
|
|
34226
35604
|
* Permissions generator for the Zed editor.
|
|
34227
35605
|
*
|
|
34228
35606
|
* Zed maps tool permissions onto `agent.tool_permissions` inside its settings
|
|
@@ -34230,6 +35608,11 @@ function asRecord(value) {
|
|
|
34230
35608
|
* global). That file is shared with the MCP (`context_servers`) and ignore
|
|
34231
35609
|
* (`private_files`) features, so reads and writes merge into the existing JSON
|
|
34232
35610
|
* rather than overwriting it, and the file is never deleted.
|
|
35611
|
+
*
|
|
35612
|
+
* Zed's OS sandbox (`agent.sandbox_permissions`) and its tool-availability
|
|
35613
|
+
* profiles (`agent.profiles`) are separate enforcement layers with no canonical
|
|
35614
|
+
* counterpart; they are authored verbatim through the `zed` override and lifted
|
|
35615
|
+
* back out of the settings on import. See `ZedPermissionsOverrideSchema`.
|
|
34233
35616
|
*/
|
|
34234
35617
|
var ZedPermissions = class ZedPermissions extends ToolPermissions {
|
|
34235
35618
|
constructor(params) {
|
|
@@ -34308,6 +35691,10 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
|
|
|
34308
35691
|
existingContent,
|
|
34309
35692
|
patch: { agent: {
|
|
34310
35693
|
...agent,
|
|
35694
|
+
...buildZedOverridePatch({
|
|
35695
|
+
override: config.zed,
|
|
35696
|
+
logger
|
|
35697
|
+
}),
|
|
34311
35698
|
tool_permissions: {
|
|
34312
35699
|
...toolPermissions,
|
|
34313
35700
|
...managedDefault !== void 0 && { default: managedDefault },
|
|
@@ -34329,7 +35716,8 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
|
|
|
34329
35716
|
} catch (error) {
|
|
34330
35717
|
throw new Error(`Failed to parse Zed permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
34331
35718
|
}
|
|
34332
|
-
const
|
|
35719
|
+
const agent = asRecord(settings.agent);
|
|
35720
|
+
const toolPermissionsRaw = agent.tool_permissions;
|
|
34333
35721
|
const parsed = ZedToolPermissionsSchema.safeParse(toolPermissionsRaw ?? {});
|
|
34334
35722
|
const tools = parsed.success ? parsed.data.tools ?? {} : {};
|
|
34335
35723
|
const globalDefault = parsed.success ? parsed.data.default : void 0;
|
|
@@ -34353,7 +35741,10 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
|
|
|
34353
35741
|
for (const entry of toolPermission.always_deny ?? []) ensure(category)[entry.pattern] = "deny";
|
|
34354
35742
|
for (const entry of toolPermission.always_confirm ?? []) ensure(category)[entry.pattern] = "ask";
|
|
34355
35743
|
}
|
|
34356
|
-
|
|
35744
|
+
const zedOverride = extractZedOverride(agent);
|
|
35745
|
+
const result = { permission };
|
|
35746
|
+
if (zedOverride !== void 0) result.zed = zedOverride;
|
|
35747
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
34357
35748
|
}
|
|
34358
35749
|
validate() {
|
|
34359
35750
|
return {
|
|
@@ -34439,6 +35830,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
34439
35830
|
supportsImport: true
|
|
34440
35831
|
}
|
|
34441
35832
|
}],
|
|
35833
|
+
["copilotcli", {
|
|
35834
|
+
class: CopilotcliPermissions,
|
|
35835
|
+
meta: {
|
|
35836
|
+
supportsProject: true,
|
|
35837
|
+
supportsGlobal: true,
|
|
35838
|
+
supportsImport: true
|
|
35839
|
+
}
|
|
35840
|
+
}],
|
|
34442
35841
|
["cursor", {
|
|
34443
35842
|
class: CursorPermissions,
|
|
34444
35843
|
meta: {
|
|
@@ -36392,7 +37791,8 @@ function openaiYamlToCodexcliSection(parsed) {
|
|
|
36392
37791
|
/**
|
|
36393
37792
|
* Represents a Codex CLI skill directory.
|
|
36394
37793
|
* Codex CLI supports skills in both project mode (under $CWD/.agents/skills)
|
|
36395
|
-
* and global mode (under
|
|
37794
|
+
* and global mode (under ~/.agents/skills — Rulesync resolves the home
|
|
37795
|
+
* directory rather than $CODEX_HOME).
|
|
36396
37796
|
*/
|
|
36397
37797
|
var CodexCliSkill = class CodexCliSkill extends ToolSkill {
|
|
36398
37798
|
constructor({ outputRoot = process.cwd(), relativeDirPath = CODEXCLI_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
@@ -38428,6 +39828,145 @@ var KiroIdeSkill = class extends KiroSkill {
|
|
|
38428
39828
|
}
|
|
38429
39829
|
};
|
|
38430
39830
|
//#endregion
|
|
39831
|
+
//#region src/features/skills/musecode-skill.ts
|
|
39832
|
+
const MusecodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
39833
|
+
name: zod_mini.z.string(),
|
|
39834
|
+
description: zod_mini.z.string()
|
|
39835
|
+
});
|
|
39836
|
+
/**
|
|
39837
|
+
* Represents a Meta Muse Code skill directory.
|
|
39838
|
+
*
|
|
39839
|
+
* Muse Code reads Agent Skills as `<skill-id>/SKILL.md` directories from the
|
|
39840
|
+
* project (`.agents/skills/`) and from the user config
|
|
39841
|
+
* (`$XDG_CONFIG_HOME/muse/skills`, plus `~/.agents/skills`). rulesync emits the
|
|
39842
|
+
* project directory and, at global scope, only the XDG-default
|
|
39843
|
+
* `~/.config/muse/skills` so a skill is written exactly once. Muse Code's
|
|
39844
|
+
* compat scans of repo-local `.codex/skills` and `.claude/skills` belong to
|
|
39845
|
+
* other tools and are not emitted.
|
|
39846
|
+
* @see https://dev.meta.ai/docs/muse-code/extending.md
|
|
39847
|
+
*/
|
|
39848
|
+
var MusecodeSkill = class MusecodeSkill extends ToolSkill {
|
|
39849
|
+
constructor({ outputRoot = process.cwd(), relativeDirPath = MUSECODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
39850
|
+
super({
|
|
39851
|
+
outputRoot,
|
|
39852
|
+
relativeDirPath,
|
|
39853
|
+
dirName,
|
|
39854
|
+
mainFile: {
|
|
39855
|
+
name: SKILL_FILE_NAME,
|
|
39856
|
+
body,
|
|
39857
|
+
frontmatter: { ...frontmatter }
|
|
39858
|
+
},
|
|
39859
|
+
otherFiles,
|
|
39860
|
+
global
|
|
39861
|
+
});
|
|
39862
|
+
if (validate) {
|
|
39863
|
+
const result = this.validate();
|
|
39864
|
+
if (!result.success) throw result.error;
|
|
39865
|
+
}
|
|
39866
|
+
}
|
|
39867
|
+
static getSettablePaths({ global = false } = {}) {
|
|
39868
|
+
return { relativeDirPath: global ? MUSECODE_GLOBAL_SKILLS_DIR_PATH : MUSECODE_SKILLS_DIR_PATH };
|
|
39869
|
+
}
|
|
39870
|
+
getFrontmatter() {
|
|
39871
|
+
return MusecodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
39872
|
+
}
|
|
39873
|
+
getBody() {
|
|
39874
|
+
return this.mainFile?.body ?? "";
|
|
39875
|
+
}
|
|
39876
|
+
validate() {
|
|
39877
|
+
if (!this.mainFile) return {
|
|
39878
|
+
success: false,
|
|
39879
|
+
error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
|
|
39880
|
+
};
|
|
39881
|
+
const result = MusecodeSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
|
|
39882
|
+
if (!result.success) return {
|
|
39883
|
+
success: false,
|
|
39884
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
|
|
39885
|
+
};
|
|
39886
|
+
return {
|
|
39887
|
+
success: true,
|
|
39888
|
+
error: null
|
|
39889
|
+
};
|
|
39890
|
+
}
|
|
39891
|
+
toRulesyncSkill() {
|
|
39892
|
+
const frontmatter = this.getFrontmatter();
|
|
39893
|
+
const rulesyncFrontmatter = {
|
|
39894
|
+
name: frontmatter.name,
|
|
39895
|
+
description: frontmatter.description,
|
|
39896
|
+
targets: ["*"]
|
|
39897
|
+
};
|
|
39898
|
+
return new RulesyncSkill({
|
|
39899
|
+
outputRoot: this.outputRoot,
|
|
39900
|
+
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
|
|
39901
|
+
dirName: this.getDirName(),
|
|
39902
|
+
frontmatter: rulesyncFrontmatter,
|
|
39903
|
+
body: this.getBody(),
|
|
39904
|
+
otherFiles: this.getOtherFiles(),
|
|
39905
|
+
validate: true,
|
|
39906
|
+
global: this.global
|
|
39907
|
+
});
|
|
39908
|
+
}
|
|
39909
|
+
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
39910
|
+
const settablePaths = MusecodeSkill.getSettablePaths({ global });
|
|
39911
|
+
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
39912
|
+
const musecodeFrontmatter = {
|
|
39913
|
+
name: rulesyncFrontmatter.name,
|
|
39914
|
+
description: rulesyncFrontmatter.description
|
|
39915
|
+
};
|
|
39916
|
+
return new MusecodeSkill({
|
|
39917
|
+
outputRoot,
|
|
39918
|
+
relativeDirPath: settablePaths.relativeDirPath,
|
|
39919
|
+
dirName: rulesyncSkill.getDirName(),
|
|
39920
|
+
frontmatter: musecodeFrontmatter,
|
|
39921
|
+
body: rulesyncSkill.getBody(),
|
|
39922
|
+
otherFiles: rulesyncSkill.getOtherFiles(),
|
|
39923
|
+
validate,
|
|
39924
|
+
global
|
|
39925
|
+
});
|
|
39926
|
+
}
|
|
39927
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
39928
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
39929
|
+
return targets.includes("*") || targets.includes("musecode");
|
|
39930
|
+
}
|
|
39931
|
+
static async fromDir(params) {
|
|
39932
|
+
const loaded = await this.loadSkillDirContent({
|
|
39933
|
+
...params,
|
|
39934
|
+
getSettablePaths: MusecodeSkill.getSettablePaths
|
|
39935
|
+
});
|
|
39936
|
+
const result = MusecodeSkillFrontmatterSchema.safeParse(loaded.frontmatter);
|
|
39937
|
+
if (!result.success) {
|
|
39938
|
+
const skillDirPath = (0, node_path.join)(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
|
|
39939
|
+
throw new Error(`Invalid frontmatter in ${(0, node_path.join)(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
|
|
39940
|
+
}
|
|
39941
|
+
return new MusecodeSkill({
|
|
39942
|
+
outputRoot: loaded.outputRoot,
|
|
39943
|
+
relativeDirPath: loaded.relativeDirPath,
|
|
39944
|
+
dirName: loaded.dirName,
|
|
39945
|
+
frontmatter: result.data,
|
|
39946
|
+
body: loaded.body,
|
|
39947
|
+
otherFiles: loaded.otherFiles,
|
|
39948
|
+
validate: true,
|
|
39949
|
+
global: loaded.global
|
|
39950
|
+
});
|
|
39951
|
+
}
|
|
39952
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
|
|
39953
|
+
const settablePaths = MusecodeSkill.getSettablePaths({ global });
|
|
39954
|
+
return new MusecodeSkill({
|
|
39955
|
+
outputRoot,
|
|
39956
|
+
relativeDirPath: relativeDirPath ?? settablePaths.relativeDirPath,
|
|
39957
|
+
dirName,
|
|
39958
|
+
frontmatter: {
|
|
39959
|
+
name: "",
|
|
39960
|
+
description: ""
|
|
39961
|
+
},
|
|
39962
|
+
body: "",
|
|
39963
|
+
otherFiles: [],
|
|
39964
|
+
validate: false,
|
|
39965
|
+
global
|
|
39966
|
+
});
|
|
39967
|
+
}
|
|
39968
|
+
};
|
|
39969
|
+
//#endregion
|
|
38431
39970
|
//#region src/features/skills/opencode-skill.ts
|
|
38432
39971
|
const OpenCodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
38433
39972
|
name: zod_mini.z.string(),
|
|
@@ -40256,6 +41795,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
40256
41795
|
supportsGlobal: true
|
|
40257
41796
|
}
|
|
40258
41797
|
}],
|
|
41798
|
+
["musecode", {
|
|
41799
|
+
class: MusecodeSkill,
|
|
41800
|
+
meta: {
|
|
41801
|
+
supportsProject: true,
|
|
41802
|
+
supportsSimulated: false,
|
|
41803
|
+
supportsGlobal: true
|
|
41804
|
+
}
|
|
41805
|
+
}],
|
|
40259
41806
|
["opencode", {
|
|
40260
41807
|
class: OpenCodeSkill,
|
|
40261
41808
|
meta: {
|
|
@@ -42776,7 +44323,7 @@ var FactorydroidSubagent = class FactorydroidSubagent extends ToolSubagent {
|
|
|
42776
44323
|
* `sub_recipes` list, which rulesync never wrote) — so those files were inert.
|
|
42777
44324
|
* The custom-agent surface is the one Goose actually reads.
|
|
42778
44325
|
*
|
|
42779
|
-
* @see https://
|
|
44326
|
+
* @see https://goose-docs.ai/docs/guides/context-engineering/custom-agents/
|
|
42780
44327
|
*/
|
|
42781
44328
|
const GooseSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
42782
44329
|
name: zod_mini.z.string(),
|
|
@@ -48110,7 +49657,7 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
48110
49657
|
* touches during a session. The separate `.goose/memories/` tree is the Memory
|
|
48111
49658
|
* extension's storage and is NOT auto-loaded as session context.
|
|
48112
49659
|
* (Verified against the official docs:
|
|
48113
|
-
* https://
|
|
49660
|
+
* https://goose-docs.ai/docs/guides/context-engineering/using-goosehints/)
|
|
48114
49661
|
*
|
|
48115
49662
|
* rulesync's topic-based non-root rules have no project subdirectory to map onto,
|
|
48116
49663
|
* so writing them under `.goose/memories/` made them effectively invisible to
|
|
@@ -48121,7 +49668,7 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
48121
49668
|
* Goose uses plain markdown files (.goosehints) without frontmatter.
|
|
48122
49669
|
*
|
|
48123
49670
|
* Global scope emits only `~/.config/goose/.goosehints`. Goose v1.41.0 (PR
|
|
48124
|
-
*
|
|
49671
|
+
* aaif-goose/goose#9736) additionally loads the vendor-neutral
|
|
48125
49672
|
* `~/.agents/AGENTS.md` alongside the config-dir hints, but rulesync
|
|
48126
49673
|
* deliberately does not emit that shared path from the goose target: the
|
|
48127
49674
|
* config-dir hints remain fully loaded (no capability loss), and the
|
|
@@ -48829,6 +50376,73 @@ var KiroIdeRule = class extends KiroRule {
|
|
|
48829
50376
|
}
|
|
48830
50377
|
};
|
|
48831
50378
|
//#endregion
|
|
50379
|
+
//#region src/features/rules/musecode-rule.ts
|
|
50380
|
+
var MusecodeRule = class MusecodeRule extends ToolRule {
|
|
50381
|
+
constructor({ fileContent, root, ...rest }) {
|
|
50382
|
+
super({
|
|
50383
|
+
...rest,
|
|
50384
|
+
fileContent,
|
|
50385
|
+
root: root ?? false
|
|
50386
|
+
});
|
|
50387
|
+
}
|
|
50388
|
+
static getSettablePaths(_options = {}) {
|
|
50389
|
+
return { root: {
|
|
50390
|
+
relativeDirPath: ".",
|
|
50391
|
+
relativeFilePath: MUSECODE_RULE_FILE_NAME
|
|
50392
|
+
} };
|
|
50393
|
+
}
|
|
50394
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
|
|
50395
|
+
const { root } = this.getSettablePaths({ global });
|
|
50396
|
+
const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
|
|
50397
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
|
|
50398
|
+
return new MusecodeRule({
|
|
50399
|
+
outputRoot,
|
|
50400
|
+
relativeDirPath: root.relativeDirPath,
|
|
50401
|
+
relativeFilePath: root.relativeFilePath,
|
|
50402
|
+
fileContent,
|
|
50403
|
+
validate,
|
|
50404
|
+
root: true
|
|
50405
|
+
});
|
|
50406
|
+
}
|
|
50407
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
50408
|
+
const { root } = this.getSettablePaths({ global });
|
|
50409
|
+
const isRoot = rulesyncRule.getFrontmatter().root ?? false;
|
|
50410
|
+
return new MusecodeRule({
|
|
50411
|
+
outputRoot,
|
|
50412
|
+
relativeDirPath: root.relativeDirPath,
|
|
50413
|
+
relativeFilePath: root.relativeFilePath,
|
|
50414
|
+
fileContent: rulesyncRule.getBody(),
|
|
50415
|
+
validate,
|
|
50416
|
+
root: isRoot
|
|
50417
|
+
});
|
|
50418
|
+
}
|
|
50419
|
+
toRulesyncRule() {
|
|
50420
|
+
return this.toRulesyncRuleDefault();
|
|
50421
|
+
}
|
|
50422
|
+
validate() {
|
|
50423
|
+
return {
|
|
50424
|
+
success: true,
|
|
50425
|
+
error: null
|
|
50426
|
+
};
|
|
50427
|
+
}
|
|
50428
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
50429
|
+
return new MusecodeRule({
|
|
50430
|
+
outputRoot,
|
|
50431
|
+
relativeDirPath,
|
|
50432
|
+
relativeFilePath,
|
|
50433
|
+
fileContent: "",
|
|
50434
|
+
validate: false,
|
|
50435
|
+
root: relativeFilePath === "AGENTS.md" && relativeDirPath === "."
|
|
50436
|
+
});
|
|
50437
|
+
}
|
|
50438
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
50439
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
50440
|
+
rulesyncRule,
|
|
50441
|
+
toolTarget: "musecode"
|
|
50442
|
+
});
|
|
50443
|
+
}
|
|
50444
|
+
};
|
|
50445
|
+
//#endregion
|
|
48832
50446
|
//#region src/features/rules/opencode-rule.ts
|
|
48833
50447
|
var OpenCodeRule = class OpenCodeRule extends ToolRule {
|
|
48834
50448
|
static getSettablePaths({ global, excludeToolDir } = {}) {
|
|
@@ -50409,6 +52023,15 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
50409
52023
|
ruleDiscoveryMode: "toon"
|
|
50410
52024
|
}
|
|
50411
52025
|
}],
|
|
52026
|
+
["musecode", {
|
|
52027
|
+
class: MusecodeRule,
|
|
52028
|
+
meta: {
|
|
52029
|
+
extension: "md",
|
|
52030
|
+
supportsGlobal: false,
|
|
52031
|
+
ruleDiscoveryMode: "auto",
|
|
52032
|
+
collisionPolicy: "fold"
|
|
52033
|
+
}
|
|
52034
|
+
}],
|
|
50412
52035
|
["opencode", {
|
|
50413
52036
|
class: OpenCodeRule,
|
|
50414
52037
|
meta: {
|