rulesync 9.0.2 → 9.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/cli/index.cjs +8 -2
- package/dist/cli/index.js +8 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-zmpFGK87.js → import-DywcWsgJ.js} +2763 -492
- package/dist/import-DywcWsgJ.js.map +1 -0
- package/dist/{import-BhqQwWah.cjs → import-cNqo0ZxT.cjs} +2772 -501
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +5 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-zmpFGK87.js.map +0 -1
|
@@ -198,6 +198,7 @@ const mcpProcessorToolTargetTuple = [
|
|
|
198
198
|
"reasonix",
|
|
199
199
|
"roo",
|
|
200
200
|
"rovodev",
|
|
201
|
+
"takt",
|
|
201
202
|
"vibe",
|
|
202
203
|
"warp",
|
|
203
204
|
"devin",
|
|
@@ -205,6 +206,7 @@ const mcpProcessorToolTargetTuple = [
|
|
|
205
206
|
];
|
|
206
207
|
const commandsProcessorToolTargetTuple = [
|
|
207
208
|
"agentsmd",
|
|
209
|
+
"antigravity-cli",
|
|
208
210
|
"antigravity-ide",
|
|
209
211
|
"augmentcode",
|
|
210
212
|
"claudecode",
|
|
@@ -224,7 +226,9 @@ const commandsProcessorToolTargetTuple = [
|
|
|
224
226
|
"opencode",
|
|
225
227
|
"pi",
|
|
226
228
|
"qwencode",
|
|
229
|
+
"reasonix",
|
|
227
230
|
"roo",
|
|
231
|
+
"rovodev",
|
|
228
232
|
"takt",
|
|
229
233
|
"devin"
|
|
230
234
|
];
|
|
@@ -309,11 +313,13 @@ const hooksProcessorToolTargetTuple = [
|
|
|
309
313
|
"deepagents",
|
|
310
314
|
"kiro",
|
|
311
315
|
"kiro-cli",
|
|
316
|
+
"kiro-ide",
|
|
312
317
|
"devin",
|
|
313
318
|
"augmentcode",
|
|
314
319
|
"junie",
|
|
315
320
|
"vibe",
|
|
316
|
-
"qwencode"
|
|
321
|
+
"qwencode",
|
|
322
|
+
"reasonix"
|
|
317
323
|
];
|
|
318
324
|
const permissionsProcessorToolTargetTuple = [
|
|
319
325
|
"amp",
|
|
@@ -324,16 +330,19 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
324
330
|
"cline",
|
|
325
331
|
"codexcli",
|
|
326
332
|
"cursor",
|
|
333
|
+
"devin",
|
|
327
334
|
"factorydroid",
|
|
328
335
|
"goose",
|
|
329
336
|
"grokcli",
|
|
330
337
|
"hermesagent",
|
|
338
|
+
"junie",
|
|
331
339
|
"kilo",
|
|
332
340
|
"kiro",
|
|
333
341
|
"kiro-cli",
|
|
334
342
|
"kiro-ide",
|
|
335
343
|
"opencode",
|
|
336
344
|
"qwencode",
|
|
345
|
+
"reasonix",
|
|
337
346
|
"rovodev",
|
|
338
347
|
"takt",
|
|
339
348
|
"vibe",
|
|
@@ -1373,7 +1382,7 @@ function extractConfigFileTargets(targets) {
|
|
|
1373
1382
|
* Type guard to check if a value is a plain object (Record<string, unknown>).
|
|
1374
1383
|
* This excludes arrays and null values.
|
|
1375
1384
|
*/
|
|
1376
|
-
function isRecord
|
|
1385
|
+
function isRecord(value) {
|
|
1377
1386
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1378
1387
|
}
|
|
1379
1388
|
/**
|
|
@@ -1388,7 +1397,7 @@ function isRecord$1(value) {
|
|
|
1388
1397
|
* malicious accessor descriptors.
|
|
1389
1398
|
*/
|
|
1390
1399
|
function isPlainObject(value) {
|
|
1391
|
-
if (!isRecord
|
|
1400
|
+
if (!isRecord(value)) return false;
|
|
1392
1401
|
const proto = Object.getPrototypeOf(value);
|
|
1393
1402
|
return proto === null || proto === Object.prototype;
|
|
1394
1403
|
}
|
|
@@ -1739,6 +1748,17 @@ var ToolCommand = class extends AiFile {
|
|
|
1739
1748
|
throw new Error("Please implement this method in the subclass.");
|
|
1740
1749
|
}
|
|
1741
1750
|
/**
|
|
1751
|
+
* Optional hook for tools whose commands are not purely one-file-per-command
|
|
1752
|
+
* (e.g. Rovo Dev's `prompts.yml` manifest that indexes every saved-prompt
|
|
1753
|
+
* content file). Given the full set of ToolCommand instances just generated
|
|
1754
|
+
* for this target, returns any additional shared/aggregate files that must
|
|
1755
|
+
* be written alongside them. Most tools don't need this and inherit the
|
|
1756
|
+
* empty default (mirrors `ToolHooks.getAuxiliaryFiles`).
|
|
1757
|
+
*/
|
|
1758
|
+
static async getAuxiliaryFiles(_params) {
|
|
1759
|
+
return [];
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1742
1762
|
* Convert a RulesyncCommand to the tool-specific command format.
|
|
1743
1763
|
*
|
|
1744
1764
|
* This method should:
|
|
@@ -1897,16 +1917,25 @@ var AgentsmdCommand = class AgentsmdCommand extends SimulatedCommand {
|
|
|
1897
1917
|
}
|
|
1898
1918
|
};
|
|
1899
1919
|
//#endregion
|
|
1900
|
-
//#region src/constants/antigravity-
|
|
1901
|
-
const
|
|
1902
|
-
const
|
|
1903
|
-
const
|
|
1904
|
-
const
|
|
1905
|
-
const
|
|
1906
|
-
const
|
|
1907
|
-
const
|
|
1908
|
-
const
|
|
1909
|
-
const
|
|
1920
|
+
//#region src/constants/antigravity-paths.ts
|
|
1921
|
+
const ANTIGRAVITY_DIR = ".agents";
|
|
1922
|
+
const ANTIGRAVITY_SKILLS_DIR_PATH = join(ANTIGRAVITY_DIR, "skills");
|
|
1923
|
+
const ANTIGRAVITY_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_DIR, "workflows");
|
|
1924
|
+
const ANTIGRAVITY_MCP_FILE_NAME = "mcp_config.json";
|
|
1925
|
+
const ANTIGRAVITY_HOOKS_FILE_NAME = "hooks.json";
|
|
1926
|
+
const ANTIGRAVITY_IGNORE_FILE_NAME = ".geminiignore";
|
|
1927
|
+
const ANTIGRAVITY_GEMINI_DIR = ".gemini";
|
|
1928
|
+
const ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR = "antigravity-cli";
|
|
1929
|
+
const ANTIGRAVITY_CLI_PERMISSIONS_DIR_PATH = join(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR);
|
|
1930
|
+
const ANTIGRAVITY_CLI_PERMISSIONS_FILE_NAME = "settings.json";
|
|
1931
|
+
const ANTIGRAVITY_CLI_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR, "global_workflows");
|
|
1932
|
+
const ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR = "config";
|
|
1933
|
+
const ANTIGRAVITY_GLOBAL_CONFIG_DIR_PATH = join(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR);
|
|
1934
|
+
//#endregion
|
|
1935
|
+
//#region src/constants/antigravity-cli-paths.ts
|
|
1936
|
+
const ANTIGRAVITY_AGENTS_DIR = ANTIGRAVITY_DIR;
|
|
1937
|
+
const ANTIGRAVITY_RULE_FILE_NAME = "AGENTS.md";
|
|
1938
|
+
const ANTIGRAVITY_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
|
|
1910
1939
|
//#endregion
|
|
1911
1940
|
//#region src/features/commands/antigravity-command.ts
|
|
1912
1941
|
const AntigravityWorkflowFrontmatterSchema = z.looseObject({
|
|
@@ -2080,7 +2109,7 @@ var AntigravitySharedCommand = class extends ToolCommand {
|
|
|
2080
2109
|
}
|
|
2081
2110
|
static extractAntigravityConfig(rulesyncCommand) {
|
|
2082
2111
|
const antigravity = rulesyncCommand.getFrontmatter().antigravity;
|
|
2083
|
-
return isRecord
|
|
2112
|
+
return isRecord(antigravity) ? antigravity : void 0;
|
|
2084
2113
|
}
|
|
2085
2114
|
static resolveTrigger(rulesyncCommand, antigravityConfig) {
|
|
2086
2115
|
const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
|
|
@@ -2162,6 +2191,45 @@ var AntigravitySharedCommand = class extends ToolCommand {
|
|
|
2162
2191
|
}
|
|
2163
2192
|
};
|
|
2164
2193
|
//#endregion
|
|
2194
|
+
//#region src/features/commands/antigravity-cli-command.ts
|
|
2195
|
+
/**
|
|
2196
|
+
* Command (workflow) generator for the Google Antigravity CLI (`agy`, Antigravity 2.0).
|
|
2197
|
+
*
|
|
2198
|
+
* Shares all body and frontmatter handling with {@link AntigravitySharedCommand};
|
|
2199
|
+
* the CLI reads project workflows from the same `.agents/workflows/` directory as
|
|
2200
|
+
* the IDE (the shared Antigravity 2.0 harness), but keeps its own global
|
|
2201
|
+
* workflows tree at `~/.gemini/antigravity-cli/global_workflows/` (mirroring the
|
|
2202
|
+
* CLI's global skills tree). It answers to the `antigravity-cli` target.
|
|
2203
|
+
*/
|
|
2204
|
+
var AntigravityCliCommand = class extends AntigravitySharedCommand {
|
|
2205
|
+
static getProjectRelativeDirPath() {
|
|
2206
|
+
return ANTIGRAVITY_WORKFLOWS_DIR_PATH;
|
|
2207
|
+
}
|
|
2208
|
+
static getGlobalRelativeDirPath() {
|
|
2209
|
+
return ANTIGRAVITY_CLI_GLOBAL_WORKFLOWS_DIR_PATH;
|
|
2210
|
+
}
|
|
2211
|
+
getToolTargetName() {
|
|
2212
|
+
return "antigravity-cli";
|
|
2213
|
+
}
|
|
2214
|
+
static isTargetedByRulesyncCommand(rulesyncCommand) {
|
|
2215
|
+
return this.isTargetedByRulesyncCommandDefault({
|
|
2216
|
+
rulesyncCommand,
|
|
2217
|
+
toolTarget: "antigravity-cli"
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
2221
|
+
//#endregion
|
|
2222
|
+
//#region src/constants/antigravity-ide-paths.ts
|
|
2223
|
+
const ANTIGRAVITY_IDE_AGENTS_DIR = ".agents";
|
|
2224
|
+
const ANTIGRAVITY_IDE_COMMANDS_DIR_PATH = join(ANTIGRAVITY_IDE_AGENTS_DIR, "workflows");
|
|
2225
|
+
const ANTIGRAVITY_IDE_RULE_FILE_NAME = "AGENTS.md";
|
|
2226
|
+
const ANTIGRAVITY_IDE_GEMINI_DIR = ".gemini";
|
|
2227
|
+
const ANTIGRAVITY_IDE_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
|
|
2228
|
+
const ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR = "config";
|
|
2229
|
+
const ANTIGRAVITY_IDE_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_IDE_GEMINI_DIR, "antigravity", "global_workflows");
|
|
2230
|
+
const ANTIGRAVITY_IDE_PERMISSIONS_DIR = ".antigravity";
|
|
2231
|
+
const ANTIGRAVITY_IDE_PERMISSIONS_FILE_NAME = "settings.json";
|
|
2232
|
+
//#endregion
|
|
2165
2233
|
//#region src/features/commands/antigravity-ide-command.ts
|
|
2166
2234
|
/**
|
|
2167
2235
|
* Command (workflow) generator for the Google Antigravity IDE (Antigravity 2.0).
|
|
@@ -2546,6 +2614,16 @@ const CodexcliCommandFrontmatterSchema = z.looseObject({
|
|
|
2546
2614
|
description: z.optional(z.string()),
|
|
2547
2615
|
"argument-hint": z.optional(z.string())
|
|
2548
2616
|
});
|
|
2617
|
+
/**
|
|
2618
|
+
* Generates Codex CLI's global-only custom prompts under `~/.codex/prompts/*.md`.
|
|
2619
|
+
*
|
|
2620
|
+
* Note: upstream Codex docs now state "Custom prompts are deprecated. Use skills for
|
|
2621
|
+
* reusable instructions" (https://developers.openai.com/codex/custom-prompts). No removal
|
|
2622
|
+
* date has been announced and custom prompts remain functional, so this class's
|
|
2623
|
+
* generation behavior is unchanged. Prefer rulesync's `codexcli` skills support
|
|
2624
|
+
* (see `src/features/skills/codexcli-skill.ts`) for new reusable instructions going
|
|
2625
|
+
* forward; this class is kept for users who still rely on custom prompts.
|
|
2626
|
+
*/
|
|
2549
2627
|
var CodexcliCommand = class CodexcliCommand extends ToolCommand {
|
|
2550
2628
|
frontmatter;
|
|
2551
2629
|
body;
|
|
@@ -2669,7 +2747,7 @@ const COPILOTCLI_PROJECT_MCP_FILE_NAME = "mcp.json";
|
|
|
2669
2747
|
const COPILOTCLI_AGENTS_DIR_PATH = join(COPILOT_DIR, "agents");
|
|
2670
2748
|
const COPILOTCLI_HOOKS_DIR_PATH = join(COPILOT_DIR, "hooks");
|
|
2671
2749
|
const COPILOTCLI_HOOKS_FILE_NAME = "copilotcli-hooks.json";
|
|
2672
|
-
const
|
|
2750
|
+
const COPILOT_SKILLS_GLOBAL_DIR_PATH = join(COPILOT_DIR, "skills");
|
|
2673
2751
|
//#endregion
|
|
2674
2752
|
//#region src/features/commands/copilot-command.ts
|
|
2675
2753
|
const CopilotCommandFrontmatterSchema = z.looseObject({
|
|
@@ -2919,20 +2997,17 @@ var CursorCommand = class CursorCommand extends ToolCommand {
|
|
|
2919
2997
|
//#endregion
|
|
2920
2998
|
//#region src/constants/devin-paths.ts
|
|
2921
2999
|
const DEVIN_DIR = ".devin";
|
|
2922
|
-
const
|
|
2923
|
-
const CODEIUM_DIR = ".codeium";
|
|
2924
|
-
const WINDSURF_SUBDIR = "windsurf";
|
|
2925
|
-
const CODEIUM_WINDSURF_DIR = join(CODEIUM_DIR, WINDSURF_SUBDIR);
|
|
2926
|
-
const WINDSURF_MEMORIES_SUBDIR = join(WINDSURF_SUBDIR, "memories");
|
|
3000
|
+
const CODEIUM_WINDSURF_DIR = join(".codeium", "windsurf");
|
|
2927
3001
|
const DEVIN_WORKFLOWS_DIR_PATH = join(DEVIN_DIR, "workflows");
|
|
2928
3002
|
const DEVIN_SKILLS_DIR_PATH = join(DEVIN_DIR, "skills");
|
|
2929
3003
|
const DEVIN_AGENTS_DIR_PATH = join(DEVIN_DIR, "agents");
|
|
2930
|
-
const
|
|
3004
|
+
const DEVIN_GLOBAL_CONFIG_DIR_PATH = join(".config", "devin");
|
|
3005
|
+
const DEVIN_GLOBAL_AGENTS_DIR_PATH = join(DEVIN_GLOBAL_CONFIG_DIR_PATH, "agents");
|
|
2931
3006
|
const CODEIUM_WINDSURF_GLOBAL_WORKFLOWS_DIR_PATH = join(CODEIUM_WINDSURF_DIR, "global_workflows");
|
|
2932
3007
|
const CODEIUM_WINDSURF_SKILLS_DIR_PATH = join(CODEIUM_WINDSURF_DIR, "skills");
|
|
2933
|
-
const
|
|
2934
|
-
const
|
|
2935
|
-
const
|
|
3008
|
+
const DEVIN_CONFIG_FILE_NAME = "config.json";
|
|
3009
|
+
const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
|
|
3010
|
+
const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
|
|
2936
3011
|
const DEVIN_IGNORE_FILE_NAME = ".devinignore";
|
|
2937
3012
|
const DEVIN_LEGACY_IGNORE_FILE_NAME = ".codeiumignore";
|
|
2938
3013
|
//#endregion
|
|
@@ -3181,6 +3256,8 @@ const GOOSE_MCP_FILE_NAME = "config.yaml";
|
|
|
3181
3256
|
const GOOSE_PERMISSIONS_FILE_NAME = "permission.yaml";
|
|
3182
3257
|
const GOOSE_HOOKS_DIR_PATH = join(".agents", "plugins", "rulesync", "hooks");
|
|
3183
3258
|
const GOOSE_HOOKS_FILE_NAME = "hooks.json";
|
|
3259
|
+
const GOOSE_PLUGIN_MCP_DIR_PATH = join(".agents", "plugins", "rulesync");
|
|
3260
|
+
const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";
|
|
3184
3261
|
const GOOSE_SKILLS_DIR_PATH = join(GOOSE_DIR, "skills");
|
|
3185
3262
|
const GOOSE_RECIPES_DIR_PATH = join(GOOSE_DIR, "recipes");
|
|
3186
3263
|
const GOOSE_GLOBAL_RECIPES_DIR_PATH = join(GOOSE_GLOBAL_DIR, "recipes");
|
|
@@ -3438,6 +3515,7 @@ const JUNIE_ALT_AGENTS_DIR_PATH = ".agents";
|
|
|
3438
3515
|
const JUNIE_MCP_DIR_PATH = join(JUNIE_DIR, "mcp");
|
|
3439
3516
|
const JUNIE_MCP_FILE_NAME = "mcp.json";
|
|
3440
3517
|
const JUNIE_HOOKS_FILE_NAME = "config.json";
|
|
3518
|
+
const JUNIE_PERMISSIONS_FILE_NAME = "allowlist.json";
|
|
3441
3519
|
const JUNIE_IGNORE_FILE_NAME = ".aiignore";
|
|
3442
3520
|
const JUNIE_RULE_FILE_NAME = "AGENTS.md";
|
|
3443
3521
|
const JUNIE_LEGACY_RULE_FILE_NAME = "guidelines.md";
|
|
@@ -3690,6 +3768,15 @@ const KIRO_SKILLS_DIR_PATH = join(KIRO_DIR, "skills");
|
|
|
3690
3768
|
const KIRO_SETTINGS_DIR_PATH = join(KIRO_DIR, "settings");
|
|
3691
3769
|
const KIRO_AGENTS_DIR_PATH = join(KIRO_DIR, "agents");
|
|
3692
3770
|
const KIRO_HOOKS_FILE_NAME = "default.json";
|
|
3771
|
+
/**
|
|
3772
|
+
* Kiro IDE 1.0 stores hooks as structured JSON files in `.kiro/hooks/`
|
|
3773
|
+
* (workspace) and `~/.kiro/hooks/` (user). A single file may declare multiple
|
|
3774
|
+
* hooks in its `hooks` array, so rulesync emits all generated hooks into one
|
|
3775
|
+
* `rulesync.json` file per scope.
|
|
3776
|
+
* @see https://kiro.dev/docs/hooks/
|
|
3777
|
+
*/
|
|
3778
|
+
const KIRO_IDE_HOOKS_DIR_PATH = join(KIRO_DIR, "hooks");
|
|
3779
|
+
const KIRO_IDE_HOOKS_FILE_NAME = "rulesync.json";
|
|
3693
3780
|
const KIRO_MCP_FILE_NAME = "mcp.json";
|
|
3694
3781
|
const KIRO_IGNORE_FILE_NAME = ".kiroignore";
|
|
3695
3782
|
//#endregion
|
|
@@ -4269,6 +4356,135 @@ var QwencodeCommand = class QwencodeCommand extends ToolCommand {
|
|
|
4269
4356
|
}
|
|
4270
4357
|
};
|
|
4271
4358
|
//#endregion
|
|
4359
|
+
//#region src/constants/reasonix-paths.ts
|
|
4360
|
+
const REASONIX_PROJECT_MCP_FILE_NAME = "reasonix.toml";
|
|
4361
|
+
const REASONIX_GLOBAL_DIR = ".reasonix";
|
|
4362
|
+
const REASONIX_GLOBAL_MCP_FILE_NAME = "config.toml";
|
|
4363
|
+
const REASONIX_PROJECT_PERMISSIONS_FILE_NAME = REASONIX_PROJECT_MCP_FILE_NAME;
|
|
4364
|
+
const REASONIX_GLOBAL_PERMISSIONS_FILE_NAME = REASONIX_GLOBAL_MCP_FILE_NAME;
|
|
4365
|
+
const REASONIX_DIR = REASONIX_GLOBAL_DIR;
|
|
4366
|
+
const REASONIX_SETTINGS_FILE_NAME = "settings.json";
|
|
4367
|
+
const REASONIX_COMMANDS_DIR_PATH = join(REASONIX_DIR, "commands");
|
|
4368
|
+
//#endregion
|
|
4369
|
+
//#region src/features/commands/reasonix-command.ts
|
|
4370
|
+
/**
|
|
4371
|
+
* Reasonix custom slash commands are Markdown files under `.reasonix/commands/`
|
|
4372
|
+
* (project) / `~/.reasonix/commands/` (global) — directly analogous to Claude
|
|
4373
|
+
* Code's `.claude/commands/` (Reasonix explicitly copies Claude Code's
|
|
4374
|
+
* conventions). Frontmatter supports `description` and `argument-hint`; the
|
|
4375
|
+
* body uses the same `$ARGUMENTS` / `$1`…`$N` placeholder syntax rulesync's
|
|
4376
|
+
* universal command-body syntax already targets.
|
|
4377
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/GUIDE.md
|
|
4378
|
+
*/
|
|
4379
|
+
const ReasonixCommandFrontmatterSchema = z.looseObject({
|
|
4380
|
+
description: z.optional(z.string()),
|
|
4381
|
+
"argument-hint": z.optional(z.string())
|
|
4382
|
+
});
|
|
4383
|
+
var ReasonixCommand = class ReasonixCommand extends ToolCommand {
|
|
4384
|
+
frontmatter;
|
|
4385
|
+
body;
|
|
4386
|
+
constructor({ frontmatter, body, ...rest }) {
|
|
4387
|
+
if (rest.validate) {
|
|
4388
|
+
const result = ReasonixCommandFrontmatterSchema.safeParse(frontmatter);
|
|
4389
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
4390
|
+
}
|
|
4391
|
+
super({
|
|
4392
|
+
...rest,
|
|
4393
|
+
fileContent: stringifyFrontmatter(body, frontmatter)
|
|
4394
|
+
});
|
|
4395
|
+
this.frontmatter = frontmatter;
|
|
4396
|
+
this.body = body;
|
|
4397
|
+
}
|
|
4398
|
+
static getSettablePaths(_options = {}) {
|
|
4399
|
+
return { relativeDirPath: REASONIX_COMMANDS_DIR_PATH };
|
|
4400
|
+
}
|
|
4401
|
+
getBody() {
|
|
4402
|
+
return this.body;
|
|
4403
|
+
}
|
|
4404
|
+
getFrontmatter() {
|
|
4405
|
+
return this.frontmatter;
|
|
4406
|
+
}
|
|
4407
|
+
toRulesyncCommand() {
|
|
4408
|
+
const { description, ...restFields } = this.frontmatter;
|
|
4409
|
+
const rulesyncFrontmatter = {
|
|
4410
|
+
targets: ["*"],
|
|
4411
|
+
description,
|
|
4412
|
+
...Object.keys(restFields).length > 0 && { reasonix: restFields }
|
|
4413
|
+
};
|
|
4414
|
+
const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter);
|
|
4415
|
+
return new RulesyncCommand({
|
|
4416
|
+
outputRoot: ".",
|
|
4417
|
+
frontmatter: rulesyncFrontmatter,
|
|
4418
|
+
body: this.body,
|
|
4419
|
+
relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
|
|
4420
|
+
relativeFilePath: this.relativeFilePath,
|
|
4421
|
+
fileContent,
|
|
4422
|
+
validate: true
|
|
4423
|
+
});
|
|
4424
|
+
}
|
|
4425
|
+
static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
|
|
4426
|
+
const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
|
|
4427
|
+
const reasonixFields = rulesyncFrontmatter.reasonix ?? {};
|
|
4428
|
+
return new ReasonixCommand({
|
|
4429
|
+
outputRoot,
|
|
4430
|
+
frontmatter: {
|
|
4431
|
+
description: rulesyncFrontmatter.description,
|
|
4432
|
+
...reasonixFields
|
|
4433
|
+
},
|
|
4434
|
+
body: rulesyncCommand.getBody(),
|
|
4435
|
+
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
4436
|
+
relativeFilePath: rulesyncCommand.getRelativeFilePath(),
|
|
4437
|
+
validate
|
|
4438
|
+
});
|
|
4439
|
+
}
|
|
4440
|
+
validate() {
|
|
4441
|
+
if (!this.frontmatter) return {
|
|
4442
|
+
success: true,
|
|
4443
|
+
error: null
|
|
4444
|
+
};
|
|
4445
|
+
const result = ReasonixCommandFrontmatterSchema.safeParse(this.frontmatter);
|
|
4446
|
+
if (result.success) return {
|
|
4447
|
+
success: true,
|
|
4448
|
+
error: null
|
|
4449
|
+
};
|
|
4450
|
+
else return {
|
|
4451
|
+
success: false,
|
|
4452
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
4453
|
+
};
|
|
4454
|
+
}
|
|
4455
|
+
static isTargetedByRulesyncCommand(rulesyncCommand) {
|
|
4456
|
+
return this.isTargetedByRulesyncCommandDefault({
|
|
4457
|
+
rulesyncCommand,
|
|
4458
|
+
toolTarget: "reasonix"
|
|
4459
|
+
});
|
|
4460
|
+
}
|
|
4461
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
4462
|
+
const paths = this.getSettablePaths({ global });
|
|
4463
|
+
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
4464
|
+
const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
4465
|
+
const result = ReasonixCommandFrontmatterSchema.safeParse(frontmatter);
|
|
4466
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
4467
|
+
return new ReasonixCommand({
|
|
4468
|
+
outputRoot,
|
|
4469
|
+
relativeDirPath: paths.relativeDirPath,
|
|
4470
|
+
relativeFilePath,
|
|
4471
|
+
frontmatter: result.data,
|
|
4472
|
+
body: content.trim(),
|
|
4473
|
+
validate
|
|
4474
|
+
});
|
|
4475
|
+
}
|
|
4476
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
4477
|
+
return new ReasonixCommand({
|
|
4478
|
+
outputRoot,
|
|
4479
|
+
relativeDirPath,
|
|
4480
|
+
relativeFilePath,
|
|
4481
|
+
frontmatter: { description: "" },
|
|
4482
|
+
body: "",
|
|
4483
|
+
validate: false
|
|
4484
|
+
});
|
|
4485
|
+
}
|
|
4486
|
+
};
|
|
4487
|
+
//#endregion
|
|
4272
4488
|
//#region src/constants/roo-paths.ts
|
|
4273
4489
|
const ROO_DIR = ".roo";
|
|
4274
4490
|
const ROO_COMMANDS_DIR_PATH = join(ROO_DIR, "commands");
|
|
@@ -4400,6 +4616,217 @@ var RooCommand = class RooCommand extends ToolCommand {
|
|
|
4400
4616
|
}
|
|
4401
4617
|
};
|
|
4402
4618
|
//#endregion
|
|
4619
|
+
//#region src/constants/rovodev-paths.ts
|
|
4620
|
+
const ROVODEV_DIR = ".rovodev";
|
|
4621
|
+
const ROVODEV_SKILLS_DIR_PATH = join(ROVODEV_DIR, "skills");
|
|
4622
|
+
const ROVODEV_SUBAGENTS_DIR_PATH = join(ROVODEV_DIR, "subagents");
|
|
4623
|
+
const ROVODEV_MODULAR_RULES_DIR_PATH = join(ROVODEV_DIR, ".rulesync", "modular-rules");
|
|
4624
|
+
const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
|
|
4625
|
+
const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
|
|
4626
|
+
const ROVODEV_MCP_FILE_NAME = "mcp.json";
|
|
4627
|
+
const ROVODEV_CONFIG_FILE_NAME = "config.yml";
|
|
4628
|
+
const ROVODEV_AGENTS_SKILLS_DIR_PATH = join(".agents", "skills");
|
|
4629
|
+
const ROVODEV_PROMPTS_FILE_NAME = "prompts.yml";
|
|
4630
|
+
const ROVODEV_PROMPTS_DIR_PATH = join(ROVODEV_DIR, "prompts");
|
|
4631
|
+
//#endregion
|
|
4632
|
+
//#region src/types/tool-file.ts
|
|
4633
|
+
var ToolFile = class extends AiFile {};
|
|
4634
|
+
//#endregion
|
|
4635
|
+
//#region src/features/commands/rovodev-command.ts
|
|
4636
|
+
/**
|
|
4637
|
+
* Rovo Dev CLI "saved prompts": a file-based custom-command surface made of a
|
|
4638
|
+
* `prompts.yml` manifest (`{ name, description, content_file }` entries) plus
|
|
4639
|
+
* per-prompt Markdown content files, discovered in repo-root `.rovodev/`, cwd
|
|
4640
|
+
* `.rovodev/`, and global `~/.rovodev/`, and invoked via `/prompts [title] [extra]`.
|
|
4641
|
+
*
|
|
4642
|
+
* This class represents a single prompt's **content file** — pure Markdown,
|
|
4643
|
+
* no frontmatter — written to `.rovodev/prompts/<name>.md` (project) or
|
|
4644
|
+
* `~/.rovodev/prompts/<name>.md` (global). The `name`/`description` are kept
|
|
4645
|
+
* on the instance (not serialized into the content file itself) so that
|
|
4646
|
+
* {@link RovodevCommand.getAuxiliaryFiles} can build the sibling
|
|
4647
|
+
* `prompts.yml` manifest that indexes every generated prompt.
|
|
4648
|
+
*
|
|
4649
|
+
* @see https://support.atlassian.com/rovo/docs/save-and-reuse-a-prompt-in-rovo-dev-cli/
|
|
4650
|
+
* @see https://support.atlassian.com/rovo/docs/rovo-dev-cli-commands/
|
|
4651
|
+
*/
|
|
4652
|
+
var RovodevCommand = class RovodevCommand extends ToolCommand {
|
|
4653
|
+
name;
|
|
4654
|
+
description;
|
|
4655
|
+
body;
|
|
4656
|
+
constructor({ name, description, body, ...rest }) {
|
|
4657
|
+
super({
|
|
4658
|
+
...rest,
|
|
4659
|
+
fileContent: body
|
|
4660
|
+
});
|
|
4661
|
+
this.name = name;
|
|
4662
|
+
this.description = description;
|
|
4663
|
+
this.body = body;
|
|
4664
|
+
if (rest.validate) {
|
|
4665
|
+
const result = this.validate();
|
|
4666
|
+
if (!result.success) throw result.error;
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4669
|
+
static getSettablePaths(_options = {}) {
|
|
4670
|
+
return { relativeDirPath: ROVODEV_PROMPTS_DIR_PATH };
|
|
4671
|
+
}
|
|
4672
|
+
getName() {
|
|
4673
|
+
return this.name;
|
|
4674
|
+
}
|
|
4675
|
+
getDescription() {
|
|
4676
|
+
return this.description;
|
|
4677
|
+
}
|
|
4678
|
+
getBody() {
|
|
4679
|
+
return this.body;
|
|
4680
|
+
}
|
|
4681
|
+
validate() {
|
|
4682
|
+
if (!this.name) return {
|
|
4683
|
+
success: false,
|
|
4684
|
+
error: /* @__PURE__ */ new Error(`${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: Rovo Dev saved-prompt name must not be empty`)
|
|
4685
|
+
};
|
|
4686
|
+
return {
|
|
4687
|
+
success: true,
|
|
4688
|
+
error: null
|
|
4689
|
+
};
|
|
4690
|
+
}
|
|
4691
|
+
toRulesyncCommand() {
|
|
4692
|
+
const rulesyncFrontmatter = {
|
|
4693
|
+
targets: ["*"],
|
|
4694
|
+
description: this.description
|
|
4695
|
+
};
|
|
4696
|
+
const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter);
|
|
4697
|
+
return new RulesyncCommand({
|
|
4698
|
+
outputRoot: ".",
|
|
4699
|
+
frontmatter: rulesyncFrontmatter,
|
|
4700
|
+
body: this.body,
|
|
4701
|
+
relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
|
|
4702
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
4703
|
+
fileContent,
|
|
4704
|
+
validate: true
|
|
4705
|
+
});
|
|
4706
|
+
}
|
|
4707
|
+
static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
|
|
4708
|
+
const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
|
|
4709
|
+
const relativeFilePath = rulesyncCommand.getRelativeFilePath();
|
|
4710
|
+
const name = basename(relativeFilePath, ".md");
|
|
4711
|
+
const paths = this.getSettablePaths({ global });
|
|
4712
|
+
return new RovodevCommand({
|
|
4713
|
+
outputRoot,
|
|
4714
|
+
name,
|
|
4715
|
+
description: rulesyncFrontmatter.description ?? "",
|
|
4716
|
+
body: rulesyncCommand.getBody(),
|
|
4717
|
+
relativeDirPath: paths.relativeDirPath,
|
|
4718
|
+
relativeFilePath,
|
|
4719
|
+
validate,
|
|
4720
|
+
global
|
|
4721
|
+
});
|
|
4722
|
+
}
|
|
4723
|
+
static isTargetedByRulesyncCommand(rulesyncCommand) {
|
|
4724
|
+
return this.isTargetedByRulesyncCommandDefault({
|
|
4725
|
+
rulesyncCommand,
|
|
4726
|
+
toolTarget: "rovodev"
|
|
4727
|
+
});
|
|
4728
|
+
}
|
|
4729
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
4730
|
+
const paths = this.getSettablePaths({ global });
|
|
4731
|
+
const body = (await readFileContent(join(outputRoot, paths.relativeDirPath, relativeFilePath))).trim();
|
|
4732
|
+
const name = basename(relativeFilePath, ".md");
|
|
4733
|
+
return new RovodevCommand({
|
|
4734
|
+
outputRoot,
|
|
4735
|
+
name,
|
|
4736
|
+
description: await lookupPromptDescription({
|
|
4737
|
+
outputRoot,
|
|
4738
|
+
relativeFilePath,
|
|
4739
|
+
name
|
|
4740
|
+
}),
|
|
4741
|
+
body,
|
|
4742
|
+
relativeDirPath: paths.relativeDirPath,
|
|
4743
|
+
relativeFilePath,
|
|
4744
|
+
validate,
|
|
4745
|
+
global
|
|
4746
|
+
});
|
|
4747
|
+
}
|
|
4748
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
4749
|
+
return new RovodevCommand({
|
|
4750
|
+
outputRoot,
|
|
4751
|
+
name: basename(relativeFilePath, ".md"),
|
|
4752
|
+
description: "",
|
|
4753
|
+
body: "",
|
|
4754
|
+
relativeDirPath,
|
|
4755
|
+
relativeFilePath,
|
|
4756
|
+
validate: false,
|
|
4757
|
+
global
|
|
4758
|
+
});
|
|
4759
|
+
}
|
|
4760
|
+
/**
|
|
4761
|
+
* Rebuilds `.rovodev/prompts.yml` (project) / `~/.rovodev/prompts.yml`
|
|
4762
|
+
* (global) from every `RovodevCommand` generated in this pass. The `prompts`
|
|
4763
|
+
* array is fully replaced with the current set of rulesync-managed prompts
|
|
4764
|
+
* (mirrors `RovodevMcp` fully replacing `mcpServers`); any other top-level
|
|
4765
|
+
* key in an existing manifest is preserved.
|
|
4766
|
+
*/
|
|
4767
|
+
static async getAuxiliaryFiles({ toolCommands, outputRoot = process.cwd(), global = false }) {
|
|
4768
|
+
const rovodevCommands = toolCommands.filter((command) => command instanceof RovodevCommand);
|
|
4769
|
+
if (rovodevCommands.length === 0) return [];
|
|
4770
|
+
const existingContent = await readFileContentOrNull(join(outputRoot, ROVODEV_DIR, ROVODEV_PROMPTS_FILE_NAME));
|
|
4771
|
+
let existing = {};
|
|
4772
|
+
if (existingContent) try {
|
|
4773
|
+
const parsed = load(existingContent);
|
|
4774
|
+
if (isPlainObject(parsed)) existing = parsed;
|
|
4775
|
+
} catch {}
|
|
4776
|
+
const prompts = rovodevCommands.map((command) => ({
|
|
4777
|
+
name: command.getName(),
|
|
4778
|
+
description: command.getDescription(),
|
|
4779
|
+
content_file: toPosixPath(join("prompts", command.getRelativeFilePath()))
|
|
4780
|
+
})).toSorted((a, b) => a.name.localeCompare(b.name));
|
|
4781
|
+
return [new RovodevPromptsManifest({
|
|
4782
|
+
outputRoot,
|
|
4783
|
+
relativeDirPath: ROVODEV_DIR,
|
|
4784
|
+
relativeFilePath: ROVODEV_PROMPTS_FILE_NAME,
|
|
4785
|
+
fileContent: dump({
|
|
4786
|
+
...existing,
|
|
4787
|
+
prompts
|
|
4788
|
+
}),
|
|
4789
|
+
global
|
|
4790
|
+
})];
|
|
4791
|
+
}
|
|
4792
|
+
};
|
|
4793
|
+
/**
|
|
4794
|
+
* The `prompts` entry shape read back from `content_file`, matched against
|
|
4795
|
+
* either the resolved path (relative to `prompts.yml`, i.e. the `.rovodev/`
|
|
4796
|
+
* directory) or the entry `name`, to recover the `description` that isn't
|
|
4797
|
+
* stored in the content file itself.
|
|
4798
|
+
*/
|
|
4799
|
+
async function lookupPromptDescription({ outputRoot, relativeFilePath, name }) {
|
|
4800
|
+
const manifestContent = await readFileContentOrNull(join(outputRoot, ROVODEV_DIR, ROVODEV_PROMPTS_FILE_NAME));
|
|
4801
|
+
if (!manifestContent) return "";
|
|
4802
|
+
let parsed;
|
|
4803
|
+
try {
|
|
4804
|
+
parsed = load(manifestContent);
|
|
4805
|
+
} catch {
|
|
4806
|
+
return "";
|
|
4807
|
+
}
|
|
4808
|
+
if (!isPlainObject(parsed) || !Array.isArray(parsed.prompts)) return "";
|
|
4809
|
+
const expectedContentFile = toPosixPath(join("prompts", relativeFilePath));
|
|
4810
|
+
const entry = parsed.prompts.find((candidate) => isRecord(candidate) && (candidate.content_file === expectedContentFile || candidate.name === name));
|
|
4811
|
+
return isRecord(entry) && typeof entry.description === "string" ? entry.description : "";
|
|
4812
|
+
}
|
|
4813
|
+
/**
|
|
4814
|
+
* The shared `.rovodev/prompts.yml` manifest that indexes every saved prompt.
|
|
4815
|
+
* Never deleted by orphan cleanup: it is regenerated (not individually
|
|
4816
|
+
* discovered) alongside the per-prompt content files it references.
|
|
4817
|
+
*/
|
|
4818
|
+
var RovodevPromptsManifest = class extends ToolFile {
|
|
4819
|
+
isDeletable() {
|
|
4820
|
+
return false;
|
|
4821
|
+
}
|
|
4822
|
+
validate() {
|
|
4823
|
+
return {
|
|
4824
|
+
success: true,
|
|
4825
|
+
error: null
|
|
4826
|
+
};
|
|
4827
|
+
}
|
|
4828
|
+
};
|
|
4829
|
+
//#endregion
|
|
4403
4830
|
//#region src/constants/takt-paths.ts
|
|
4404
4831
|
const TAKT_DIR = ".takt";
|
|
4405
4832
|
const TAKT_FACETS_SUBDIR = "facets";
|
|
@@ -4417,6 +4844,14 @@ const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
|
|
|
4417
4844
|
* @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
|
|
4418
4845
|
*/
|
|
4419
4846
|
const TAKT_CONFIG_FILE_NAME = "config.yaml";
|
|
4847
|
+
/**
|
|
4848
|
+
* Top-level key in Takt's `config.yaml` holding the workflow MCP transport
|
|
4849
|
+
* allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
|
|
4850
|
+
* transport must be explicitly enabled here before any workflow-defined MCP
|
|
4851
|
+
* server using it is permitted to run.
|
|
4852
|
+
* @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
|
|
4853
|
+
*/
|
|
4854
|
+
const TAKT_WORKFLOW_MCP_SERVERS_KEY = "workflow_mcp_servers";
|
|
4420
4855
|
//#endregion
|
|
4421
4856
|
//#region src/features/takt-shared.ts
|
|
4422
4857
|
/**
|
|
@@ -4592,6 +5027,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
|
|
|
4592
5027
|
supportsSubdirectory: false
|
|
4593
5028
|
}
|
|
4594
5029
|
}],
|
|
5030
|
+
["antigravity-cli", {
|
|
5031
|
+
class: AntigravityCliCommand,
|
|
5032
|
+
meta: {
|
|
5033
|
+
extension: "md",
|
|
5034
|
+
supportsProject: true,
|
|
5035
|
+
supportsGlobal: true,
|
|
5036
|
+
isSimulated: false,
|
|
5037
|
+
supportsSubdirectory: false
|
|
5038
|
+
}
|
|
5039
|
+
}],
|
|
4595
5040
|
["antigravity-ide", {
|
|
4596
5041
|
class: AntigravityIdeCommand,
|
|
4597
5042
|
meta: {
|
|
@@ -4737,7 +5182,7 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
|
|
|
4737
5182
|
meta: {
|
|
4738
5183
|
extension: "md",
|
|
4739
5184
|
supportsProject: true,
|
|
4740
|
-
supportsGlobal:
|
|
5185
|
+
supportsGlobal: true,
|
|
4741
5186
|
isSimulated: false,
|
|
4742
5187
|
supportsSubdirectory: false
|
|
4743
5188
|
}
|
|
@@ -4782,6 +5227,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
|
|
|
4782
5227
|
supportsSubdirectory: true
|
|
4783
5228
|
}
|
|
4784
5229
|
}],
|
|
5230
|
+
["reasonix", {
|
|
5231
|
+
class: ReasonixCommand,
|
|
5232
|
+
meta: {
|
|
5233
|
+
extension: "md",
|
|
5234
|
+
supportsProject: true,
|
|
5235
|
+
supportsGlobal: true,
|
|
5236
|
+
isSimulated: false,
|
|
5237
|
+
supportsSubdirectory: true
|
|
5238
|
+
}
|
|
5239
|
+
}],
|
|
4785
5240
|
["roo", {
|
|
4786
5241
|
class: RooCommand,
|
|
4787
5242
|
meta: {
|
|
@@ -4792,6 +5247,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
|
|
|
4792
5247
|
supportsSubdirectory: true
|
|
4793
5248
|
}
|
|
4794
5249
|
}],
|
|
5250
|
+
["rovodev", {
|
|
5251
|
+
class: RovodevCommand,
|
|
5252
|
+
meta: {
|
|
5253
|
+
extension: "md",
|
|
5254
|
+
supportsProject: true,
|
|
5255
|
+
supportsGlobal: true,
|
|
5256
|
+
isSimulated: false,
|
|
5257
|
+
supportsSubdirectory: false
|
|
5258
|
+
}
|
|
5259
|
+
}],
|
|
4795
5260
|
["takt", {
|
|
4796
5261
|
class: TaktCommand,
|
|
4797
5262
|
meta: {
|
|
@@ -4849,7 +5314,7 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
4849
5314
|
const rulesyncCommands = rulesyncFiles.filter((file) => file instanceof RulesyncCommand);
|
|
4850
5315
|
const factory = this.getFactory(this.toolTarget);
|
|
4851
5316
|
const flattenedPathOrigins = /* @__PURE__ */ new Map();
|
|
4852
|
-
|
|
5317
|
+
const toolCommands = rulesyncCommands.map((rulesyncCommand) => {
|
|
4853
5318
|
if (!factory.class.isTargetedByRulesyncCommand(rulesyncCommand)) return null;
|
|
4854
5319
|
const originalRelativePath = rulesyncCommand.getRelativeFilePath();
|
|
4855
5320
|
const commandToConvert = factory.meta.supportsSubdirectory ? rulesyncCommand : this.flattenRelativeFilePath(rulesyncCommand);
|
|
@@ -4865,6 +5330,14 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
4865
5330
|
global: this.global
|
|
4866
5331
|
});
|
|
4867
5332
|
}).filter((command) => command !== null);
|
|
5333
|
+
const auxiliaryFiles = await factory.class.getAuxiliaryFiles?.({
|
|
5334
|
+
toolCommands,
|
|
5335
|
+
outputRoot: this.outputRoot,
|
|
5336
|
+
global: this.global
|
|
5337
|
+
});
|
|
5338
|
+
const result = [...toolCommands];
|
|
5339
|
+
if (auxiliaryFiles && auxiliaryFiles.length > 0) result.push(...auxiliaryFiles);
|
|
5340
|
+
return result;
|
|
4868
5341
|
}
|
|
4869
5342
|
async convertToolFilesToRulesyncFiles(toolFiles) {
|
|
4870
5343
|
return toolFiles.filter((file) => file instanceof ToolCommand).map((toolCommand) => {
|
|
@@ -5023,7 +5496,14 @@ const HookDefinitionSchema = z.looseObject({
|
|
|
5023
5496
|
name: z.optional(safeString),
|
|
5024
5497
|
description: z.optional(safeString),
|
|
5025
5498
|
failClosed: z.optional(z.boolean()),
|
|
5026
|
-
sequential: z.optional(z.boolean())
|
|
5499
|
+
sequential: z.optional(z.boolean()),
|
|
5500
|
+
async: z.optional(z.boolean()),
|
|
5501
|
+
env: z.optional(z.record(z.string(), safeString)),
|
|
5502
|
+
shell: z.optional(safeString),
|
|
5503
|
+
statusMessage: z.optional(safeString),
|
|
5504
|
+
headers: z.optional(z.record(z.string(), safeString)),
|
|
5505
|
+
allowedEnvVars: z.optional(z.array(z.string())),
|
|
5506
|
+
once: z.optional(z.boolean())
|
|
5027
5507
|
});
|
|
5028
5508
|
/** Hook events supported by Cursor. */
|
|
5029
5509
|
const CURSOR_HOOK_EVENTS = [
|
|
@@ -5087,6 +5567,30 @@ const CLAUDE_HOOK_EVENTS = [
|
|
|
5087
5567
|
"elicitation",
|
|
5088
5568
|
"elicitationResult"
|
|
5089
5569
|
];
|
|
5570
|
+
/**
|
|
5571
|
+
* Hook events supported by Devin Local (native `.devin/` hooks).
|
|
5572
|
+
*
|
|
5573
|
+
* Devin Local adopts a Claude-Code-style lifecycle hooks surface. It documents
|
|
5574
|
+
* seven events: `PreToolUse`, `PostToolUse`, `PermissionRequest`,
|
|
5575
|
+
* `UserPromptSubmit`, `Stop`, `SessionStart`, and `SessionEnd`. The
|
|
5576
|
+
* tool/permission events (`PreToolUse`/`PostToolUse`/`PermissionRequest`) carry
|
|
5577
|
+
* a `matcher` (regex against `tool_name`); the session/turn events do not.
|
|
5578
|
+
*
|
|
5579
|
+
* Hooks live in `.devin/hooks.v1.json` (project, standalone — the hooks object
|
|
5580
|
+
* is the entire file) or under the `"hooks"` key of `.devin/config.json` /
|
|
5581
|
+
* `~/.config/devin/config.json`.
|
|
5582
|
+
*
|
|
5583
|
+
* @see https://docs.devin.ai/cli/extensibility/hooks/overview
|
|
5584
|
+
*/
|
|
5585
|
+
const DEVIN_HOOK_EVENTS = [
|
|
5586
|
+
"sessionStart",
|
|
5587
|
+
"sessionEnd",
|
|
5588
|
+
"preToolUse",
|
|
5589
|
+
"postToolUse",
|
|
5590
|
+
"beforeSubmitPrompt",
|
|
5591
|
+
"stop",
|
|
5592
|
+
"permissionRequest"
|
|
5593
|
+
];
|
|
5090
5594
|
/** Hook events supported by OpenCode. */
|
|
5091
5595
|
const OPENCODE_HOOK_EVENTS = [
|
|
5092
5596
|
"sessionStart",
|
|
@@ -5129,7 +5633,11 @@ const COPILOT_HOOK_EVENTS = [
|
|
|
5129
5633
|
* `sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
|
|
5130
5634
|
* `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
|
|
5131
5635
|
* `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
|
|
5132
|
-
* `notification`.
|
|
5636
|
+
* `notification`, `preMcpToolCall` ← `beforeMCPExecution`.
|
|
5637
|
+
*
|
|
5638
|
+
* `preMcpToolCall` (canonical `beforeMCPExecution`) was added in Copilot CLI
|
|
5639
|
+
* v1.0.51 (2026-05-20) for hook providers to control outgoing MCP request
|
|
5640
|
+
* metadata. https://github.com/github/copilot-cli/blob/main/changelog.md
|
|
5133
5641
|
*
|
|
5134
5642
|
* @see https://docs.github.com/en/copilot/reference/hooks-configuration
|
|
5135
5643
|
*/
|
|
@@ -5146,7 +5654,8 @@ const COPILOTCLI_HOOK_EVENTS = [
|
|
|
5146
5654
|
"afterError",
|
|
5147
5655
|
"preCompact",
|
|
5148
5656
|
"permissionRequest",
|
|
5149
|
-
"notification"
|
|
5657
|
+
"notification",
|
|
5658
|
+
"beforeMCPExecution"
|
|
5150
5659
|
];
|
|
5151
5660
|
/**
|
|
5152
5661
|
* Hook events supported by Factory Droid.
|
|
@@ -5231,6 +5740,25 @@ const KIRO_HOOK_EVENTS = [
|
|
|
5231
5740
|
"stop"
|
|
5232
5741
|
];
|
|
5233
5742
|
/**
|
|
5743
|
+
* Hook events supported by the Kiro IDE (`.kiro/hooks/*.json` v1).
|
|
5744
|
+
*
|
|
5745
|
+
* Kiro IDE 1.0 exposes PascalCase triggers. rulesync maps the canonical
|
|
5746
|
+
* lifecycle events that have a clean 1:1 IDE equivalent: `SessionStart`,
|
|
5747
|
+
* `Stop`, `UserPromptSubmit`, `PreToolUse`, and `PostToolUse`. The IDE also
|
|
5748
|
+
* documents file-event (`PostFileCreate`/`PostFileSave`/`PostFileDelete`) and
|
|
5749
|
+
* spec-task (`PreTaskExec`/`PostTaskExec`) triggers that have no canonical
|
|
5750
|
+
* equivalent; those can still be emitted verbatim via a `kiro-ide` override
|
|
5751
|
+
* block (unknown event keys pass through unchanged).
|
|
5752
|
+
* @see https://kiro.dev/docs/hooks/types/
|
|
5753
|
+
*/
|
|
5754
|
+
const KIRO_IDE_HOOK_EVENTS = [
|
|
5755
|
+
"sessionStart",
|
|
5756
|
+
"beforeSubmitPrompt",
|
|
5757
|
+
"preToolUse",
|
|
5758
|
+
"postToolUse",
|
|
5759
|
+
"stop"
|
|
5760
|
+
];
|
|
5761
|
+
/**
|
|
5234
5762
|
* Hook events supported by Google Antigravity (both the IDE and the CLI).
|
|
5235
5763
|
*
|
|
5236
5764
|
* Antigravity exposes a Claude-style hooks surface covering the five
|
|
@@ -5281,18 +5809,23 @@ const VIBE_HOOK_EVENTS = [
|
|
|
5281
5809
|
/**
|
|
5282
5810
|
* Hook events supported by JetBrains Junie CLI.
|
|
5283
5811
|
*
|
|
5284
|
-
* Junie CLI exposes
|
|
5285
|
-
* `~/.junie/config.json`: `SessionStart`, `UserPromptSubmit`, `
|
|
5286
|
-
* `
|
|
5287
|
-
*
|
|
5288
|
-
*
|
|
5289
|
-
*
|
|
5812
|
+
* Junie CLI exposes seven lifecycle events under the `"hooks"` key of
|
|
5813
|
+
* `~/.junie/config.json`: `SessionStart`, `UserPromptSubmit`, `PreToolUse`,
|
|
5814
|
+
* `Stop`, `StopFailure`, `PermissionRequest`, and `SessionEnd`. Matchers apply
|
|
5815
|
+
* to `SessionStart` (source), `PreToolUse` (tool name), `StopFailure` (error
|
|
5816
|
+
* type), `PermissionRequest` (tool name), and `SessionEnd` (reason);
|
|
5817
|
+
* `UserPromptSubmit` and `Stop` are matcher-less and always run. Only
|
|
5818
|
+
* `type: "command"` hooks are supported. Project-local hooks are ignored for
|
|
5819
|
+
* safety.
|
|
5290
5820
|
* @see https://junie.jetbrains.com/docs/junie-cli-hooks.html
|
|
5291
5821
|
*/
|
|
5292
5822
|
const JUNIE_HOOK_EVENTS = [
|
|
5293
5823
|
"sessionStart",
|
|
5294
5824
|
"beforeSubmitPrompt",
|
|
5825
|
+
"preToolUse",
|
|
5295
5826
|
"stop",
|
|
5827
|
+
"stopFailure",
|
|
5828
|
+
"permissionRequest",
|
|
5296
5829
|
"sessionEnd"
|
|
5297
5830
|
];
|
|
5298
5831
|
/**
|
|
@@ -5324,6 +5857,69 @@ const QWENCODE_HOOK_EVENTS = [
|
|
|
5324
5857
|
"todoCreated",
|
|
5325
5858
|
"todoCompleted"
|
|
5326
5859
|
];
|
|
5860
|
+
/**
|
|
5861
|
+
* Hook events supported by Reasonix.
|
|
5862
|
+
*
|
|
5863
|
+
* Reasonix's `.reasonix/settings.json` (project) / `~/.reasonix/settings.json`
|
|
5864
|
+
* (global) documents a ten-event surface (`PreToolUse`, `PostToolUse`,
|
|
5865
|
+
* `UserPromptSubmit`, `Stop`, `PostLLMCall`, `SessionStart`, `SessionEnd`,
|
|
5866
|
+
* `SubagentStop`, `Notification`, `PreCompact`), but only the four events the
|
|
5867
|
+
* upstream issue scoped in are mapped here: `PreToolUse`, `PostToolUse`,
|
|
5868
|
+
* `UserPromptSubmit` ← `beforeSubmitPrompt`, and `Stop`. `match` (Reasonix's
|
|
5869
|
+
* matcher field name) is honored only on `PreToolUse`/`PostToolUse`, matching
|
|
5870
|
+
* the canonical `matcher` field's tool-event scoping used by other adapters.
|
|
5871
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
|
|
5872
|
+
*/
|
|
5873
|
+
const REASONIX_HOOK_EVENTS = [
|
|
5874
|
+
"preToolUse",
|
|
5875
|
+
"postToolUse",
|
|
5876
|
+
"beforeSubmitPrompt",
|
|
5877
|
+
"stop"
|
|
5878
|
+
];
|
|
5879
|
+
/**
|
|
5880
|
+
* Hook events supported by Hermes Agent's native Shell Hooks system.
|
|
5881
|
+
*
|
|
5882
|
+
* Hermes validates hook events against a fixed `VALID_HOOKS` set:
|
|
5883
|
+
* `pre_tool_call`, `post_tool_call`, `pre_llm_call`, `post_llm_call`,
|
|
5884
|
+
* `pre_verify`, `on_session_start`, `on_session_end`, `on_session_finalize`,
|
|
5885
|
+
* `on_session_reset`, `subagent_start`, `subagent_stop`, `pre_gateway_dispatch`,
|
|
5886
|
+
* `pre_approval_request`, `post_approval_response`, `transform_tool_result`,
|
|
5887
|
+
* `transform_terminal_output`, `transform_llm_output`. Only the events with a
|
|
5888
|
+
* clean 1:1 canonical equivalent are mapped here; the remaining `VALID_HOOKS`
|
|
5889
|
+
* entries (`pre_verify`, `on_session_finalize`, `on_session_reset`,
|
|
5890
|
+
* `pre_gateway_dispatch`, `pre_approval_request`, `post_approval_response`, the
|
|
5891
|
+
* `transform_*` result-rewriting hooks) have no canonical rulesync equivalent,
|
|
5892
|
+
* so no canonical event maps to them.
|
|
5893
|
+
* @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
|
|
5894
|
+
*/
|
|
5895
|
+
const HERMESAGENT_HOOK_EVENTS = [
|
|
5896
|
+
"sessionStart",
|
|
5897
|
+
"sessionEnd",
|
|
5898
|
+
"preToolUse",
|
|
5899
|
+
"postToolUse",
|
|
5900
|
+
"preModelInvocation",
|
|
5901
|
+
"postModelInvocation",
|
|
5902
|
+
"subagentStart",
|
|
5903
|
+
"subagentStop"
|
|
5904
|
+
];
|
|
5905
|
+
/**
|
|
5906
|
+
* Map canonical camelCase event names to Hermes Agent's native `VALID_HOOKS`
|
|
5907
|
+
* snake_case keys under the `hooks:` block of `~/.hermes/config.yaml`.
|
|
5908
|
+
*/
|
|
5909
|
+
const CANONICAL_TO_HERMESAGENT_EVENT_NAMES = {
|
|
5910
|
+
sessionStart: "on_session_start",
|
|
5911
|
+
sessionEnd: "on_session_end",
|
|
5912
|
+
preToolUse: "pre_tool_call",
|
|
5913
|
+
postToolUse: "post_tool_call",
|
|
5914
|
+
preModelInvocation: "pre_llm_call",
|
|
5915
|
+
postModelInvocation: "post_llm_call",
|
|
5916
|
+
subagentStart: "subagent_start",
|
|
5917
|
+
subagentStop: "subagent_stop"
|
|
5918
|
+
};
|
|
5919
|
+
/**
|
|
5920
|
+
* Map Hermes Agent's native `VALID_HOOKS` keys back to canonical camelCase.
|
|
5921
|
+
*/
|
|
5922
|
+
const HERMESAGENT_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_HERMESAGENT_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5327
5923
|
const hooksRecordSchema = z.record(z.string(), z.array(HookDefinitionSchema));
|
|
5328
5924
|
/**
|
|
5329
5925
|
* Canonical hooks config (canonical event names in camelCase).
|
|
@@ -5343,12 +5939,15 @@ const HooksConfigSchema = z.looseObject({
|
|
|
5343
5939
|
deepagents: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5344
5940
|
kiro: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5345
5941
|
"kiro-cli": z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5942
|
+
"kiro-ide": z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5346
5943
|
devin: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5347
5944
|
augmentcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5348
5945
|
"antigravity-ide": z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5349
5946
|
"antigravity-cli": z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5947
|
+
hermesagent: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5350
5948
|
junie: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5351
5949
|
vibe: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5950
|
+
reasonix: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5352
5951
|
qwencode: z.optional(z.looseObject({
|
|
5353
5952
|
hooks: z.optional(hooksRecordSchema),
|
|
5354
5953
|
disableAllHooks: z.optional(z.boolean())
|
|
@@ -5394,6 +5993,26 @@ const CANONICAL_TO_CLAUDE_EVENT_NAMES = {
|
|
|
5394
5993
|
*/
|
|
5395
5994
|
const CLAUDE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CLAUDE_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5396
5995
|
/**
|
|
5996
|
+
* Map canonical camelCase event names to Devin Local PascalCase.
|
|
5997
|
+
*
|
|
5998
|
+
* Devin Local reuses the same Claude-style PascalCase event names for the
|
|
5999
|
+
* subset of events it supports.
|
|
6000
|
+
* @see https://docs.devin.ai/cli/extensibility/hooks/overview
|
|
6001
|
+
*/
|
|
6002
|
+
const CANONICAL_TO_DEVIN_EVENT_NAMES = {
|
|
6003
|
+
sessionStart: "SessionStart",
|
|
6004
|
+
sessionEnd: "SessionEnd",
|
|
6005
|
+
preToolUse: "PreToolUse",
|
|
6006
|
+
postToolUse: "PostToolUse",
|
|
6007
|
+
beforeSubmitPrompt: "UserPromptSubmit",
|
|
6008
|
+
stop: "Stop",
|
|
6009
|
+
permissionRequest: "PermissionRequest"
|
|
6010
|
+
};
|
|
6011
|
+
/**
|
|
6012
|
+
* Map Devin Local PascalCase event names to canonical camelCase.
|
|
6013
|
+
*/
|
|
6014
|
+
const DEVIN_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_DEVIN_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
6015
|
+
/**
|
|
5397
6016
|
* Map canonical camelCase event names to AugmentCode PascalCase.
|
|
5398
6017
|
* Auggie reuses the same PascalCase names as Claude for the events it supports.
|
|
5399
6018
|
*/
|
|
@@ -5524,7 +6143,8 @@ const CANONICAL_TO_COPILOTCLI_EVENT_NAMES = {
|
|
|
5524
6143
|
afterError: "errorOccurred",
|
|
5525
6144
|
preCompact: "preCompact",
|
|
5526
6145
|
permissionRequest: "permissionRequest",
|
|
5527
|
-
notification: "notification"
|
|
6146
|
+
notification: "notification",
|
|
6147
|
+
beforeMCPExecution: "preMcpToolCall"
|
|
5528
6148
|
};
|
|
5529
6149
|
/** Map GitHub Copilot CLI event names back to canonical camelCase. */
|
|
5530
6150
|
const COPILOTCLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_COPILOTCLI_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
@@ -5606,13 +6226,35 @@ const CANONICAL_TO_KIRO_EVENT_NAMES = {
|
|
|
5606
6226
|
*/
|
|
5607
6227
|
const KIRO_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_KIRO_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5608
6228
|
/**
|
|
6229
|
+
* Map canonical camelCase event names to Kiro IDE PascalCase triggers.
|
|
6230
|
+
*
|
|
6231
|
+
* Only the canonical lifecycle events with a clean IDE equivalent are mapped.
|
|
6232
|
+
* Unknown keys (e.g. IDE-only `PostFileSave`/`PreTaskExec` set via a `kiro-ide`
|
|
6233
|
+
* override) pass through unchanged.
|
|
6234
|
+
* @see https://kiro.dev/docs/hooks/types/
|
|
6235
|
+
*/
|
|
6236
|
+
const CANONICAL_TO_KIRO_IDE_EVENT_NAMES = {
|
|
6237
|
+
sessionStart: "SessionStart",
|
|
6238
|
+
beforeSubmitPrompt: "UserPromptSubmit",
|
|
6239
|
+
preToolUse: "PreToolUse",
|
|
6240
|
+
postToolUse: "PostToolUse",
|
|
6241
|
+
stop: "Stop"
|
|
6242
|
+
};
|
|
6243
|
+
/**
|
|
6244
|
+
* Map Kiro IDE PascalCase trigger names to canonical camelCase.
|
|
6245
|
+
*/
|
|
6246
|
+
const KIRO_IDE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_KIRO_IDE_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
6247
|
+
/**
|
|
5609
6248
|
* Map canonical camelCase event names to Junie PascalCase.
|
|
5610
6249
|
* Junie reuses the same PascalCase names as Claude for the events it supports.
|
|
5611
6250
|
*/
|
|
5612
6251
|
const CANONICAL_TO_JUNIE_EVENT_NAMES = {
|
|
5613
6252
|
sessionStart: "SessionStart",
|
|
5614
6253
|
beforeSubmitPrompt: "UserPromptSubmit",
|
|
6254
|
+
preToolUse: "PreToolUse",
|
|
5615
6255
|
stop: "Stop",
|
|
6256
|
+
stopFailure: "StopFailure",
|
|
6257
|
+
permissionRequest: "PermissionRequest",
|
|
5616
6258
|
sessionEnd: "SessionEnd"
|
|
5617
6259
|
};
|
|
5618
6260
|
/**
|
|
@@ -5665,19 +6307,22 @@ const CANONICAL_TO_QWENCODE_EVENT_NAMES = {
|
|
|
5665
6307
|
* Map Qwen Code PascalCase event names to canonical camelCase.
|
|
5666
6308
|
*/
|
|
5667
6309
|
const QWENCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_QWENCODE_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
const
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
6310
|
+
/**
|
|
6311
|
+
* Map canonical camelCase event names to Reasonix PascalCase.
|
|
6312
|
+
* Reasonix explicitly mirrors Claude Code's hooks model, so it reuses the same
|
|
6313
|
+
* PascalCase names for the four events rulesync maps.
|
|
6314
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
|
|
6315
|
+
*/
|
|
6316
|
+
const CANONICAL_TO_REASONIX_EVENT_NAMES = {
|
|
6317
|
+
preToolUse: "PreToolUse",
|
|
6318
|
+
postToolUse: "PostToolUse",
|
|
6319
|
+
beforeSubmitPrompt: "UserPromptSubmit",
|
|
6320
|
+
stop: "Stop"
|
|
6321
|
+
};
|
|
6322
|
+
/**
|
|
6323
|
+
* Map Reasonix PascalCase event names to canonical camelCase.
|
|
6324
|
+
*/
|
|
6325
|
+
const REASONIX_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5681
6326
|
//#endregion
|
|
5682
6327
|
//#region src/utils/prototype-pollution.ts
|
|
5683
6328
|
/**
|
|
@@ -5725,7 +6370,7 @@ function isToolMatcherEntry(x) {
|
|
|
5725
6370
|
/**
|
|
5726
6371
|
* Filter the shared canonical hooks to the supported events and merge tool overrides on top.
|
|
5727
6372
|
*/
|
|
5728
|
-
function buildEffectiveHooks({ config, toolOverrideHooks, supportedEvents }) {
|
|
6373
|
+
function buildEffectiveHooks$1({ config, toolOverrideHooks, supportedEvents }) {
|
|
5729
6374
|
const supported = new Set(supportedEvents);
|
|
5730
6375
|
const sharedHooks = {};
|
|
5731
6376
|
for (const [event, defs] of Object.entries(config.hooks)) if (supported.has(event)) sharedHooks[event] = defs;
|
|
@@ -5754,7 +6399,7 @@ function groupDefinitionsByMatcher(definitions) {
|
|
|
5754
6399
|
function applyCommandPrefix({ def, converterConfig }) {
|
|
5755
6400
|
const commandText = def.command;
|
|
5756
6401
|
const trimmedCommand = typeof commandText === "string" ? commandText.trimStart() : void 0;
|
|
5757
|
-
return converterConfig.projectDirVar !== "" && typeof trimmedCommand === "string" && !trimmedCommand.startsWith("$") && (!converterConfig.prefixDotRelativeCommandsOnly || trimmedCommand.startsWith(".")) && typeof trimmedCommand === "string" ?
|
|
6402
|
+
return converterConfig.projectDirVar !== "" && typeof trimmedCommand === "string" && !trimmedCommand.startsWith("$") && (!converterConfig.prefixDotRelativeCommandsOnly || trimmedCommand.startsWith(".")) && typeof trimmedCommand === "string" ? `"${converterConfig.projectDirVar}"/${trimmedCommand.replace(/^\.\//, "")}` : def.command;
|
|
5758
6403
|
}
|
|
5759
6404
|
/**
|
|
5760
6405
|
* Convert the definitions of a single matcher group into tool hook entries,
|
|
@@ -5787,7 +6432,7 @@ function buildToolHooks({ defs, converterConfig }) {
|
|
|
5787
6432
|
* (e.g. beforeSubmitPrompt → UserPromptSubmit).
|
|
5788
6433
|
*/
|
|
5789
6434
|
function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logger }) {
|
|
5790
|
-
const effectiveHooks = buildEffectiveHooks({
|
|
6435
|
+
const effectiveHooks = buildEffectiveHooks$1({
|
|
5791
6436
|
config,
|
|
5792
6437
|
toolOverrideHooks,
|
|
5793
6438
|
supportedEvents: converterConfig.supportedEvents
|
|
@@ -5830,7 +6475,13 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
|
|
|
5830
6475
|
*/
|
|
5831
6476
|
function stripCommandPrefix({ command, converterConfig }) {
|
|
5832
6477
|
const cmd = typeof command === "string" ? command : void 0;
|
|
5833
|
-
if (converterConfig.projectDirVar
|
|
6478
|
+
if (converterConfig.projectDirVar === "" || typeof cmd !== "string") return cmd;
|
|
6479
|
+
const quotedPrefix = `"${converterConfig.projectDirVar}"/`;
|
|
6480
|
+
if (cmd.startsWith(quotedPrefix)) return `./${cmd.slice(quotedPrefix.length)}`;
|
|
6481
|
+
if (cmd.includes(`${converterConfig.projectDirVar}/`)) {
|
|
6482
|
+
const escapedVar = converterConfig.projectDirVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6483
|
+
return cmd.replace(new RegExp(`^${escapedVar}\\/?`), "./");
|
|
6484
|
+
}
|
|
5834
6485
|
return cmd;
|
|
5835
6486
|
}
|
|
5836
6487
|
/**
|
|
@@ -5883,9 +6534,6 @@ function toolHooksToCanonical({ hooks, converterConfig }) {
|
|
|
5883
6534
|
return canonical;
|
|
5884
6535
|
}
|
|
5885
6536
|
//#endregion
|
|
5886
|
-
//#region src/types/tool-file.ts
|
|
5887
|
-
var ToolFile = class extends AiFile {};
|
|
5888
|
-
//#endregion
|
|
5889
6537
|
//#region src/features/hooks/rulesync-hooks.ts
|
|
5890
6538
|
var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
|
|
5891
6539
|
json;
|
|
@@ -6439,9 +7087,15 @@ const CODEXCLI_CONVERTER_CONFIG = {
|
|
|
6439
7087
|
passthroughFields: ["name", "description"]
|
|
6440
7088
|
};
|
|
6441
7089
|
/**
|
|
6442
|
-
* Build the content for `.codex/config.toml
|
|
6443
|
-
* Reads the existing file (if any), parses TOML,
|
|
6444
|
-
*
|
|
7090
|
+
* Build the content for `.codex/config.toml`, cleaning up the deprecated `codex_hooks` key.
|
|
7091
|
+
* Reads the existing file (if any), parses TOML, and returns the content without writing to
|
|
7092
|
+
* disk. The caller is responsible for writing via the normal write phase.
|
|
7093
|
+
*
|
|
7094
|
+
* Hooks are GA and enabled by default in Codex CLI, so `[features] hooks = true` is no longer
|
|
7095
|
+
* required and is intentionally NOT force-written here (doing so used to be harmless/idempotent,
|
|
7096
|
+
* but is now redundant and could mask a user's own `hooks = false` opt-out on a later edit).
|
|
7097
|
+
* See https://developers.openai.com/codex/hooks. Only the legacy `codex_hooks` alias — superseded
|
|
7098
|
+
* by `hooks` — is still cleaned up.
|
|
6445
7099
|
*/
|
|
6446
7100
|
async function buildCodexConfigTomlContent({ outputRoot }) {
|
|
6447
7101
|
const configPath = join(outputRoot, CODEXCLI_DIR, CODEXCLI_MCP_FILE_NAME);
|
|
@@ -6452,10 +7106,7 @@ async function buildCodexConfigTomlContent({ outputRoot }) {
|
|
|
6452
7106
|
} catch (error) {
|
|
6453
7107
|
throw new Error(`Failed to parse existing Codex CLI config at ${configPath}: ${formatError(error)}`, { cause: error });
|
|
6454
7108
|
}
|
|
6455
|
-
if (typeof configToml.features
|
|
6456
|
-
const features = configToml.features;
|
|
6457
|
-
delete features.codex_hooks;
|
|
6458
|
-
features.hooks = true;
|
|
7109
|
+
if (typeof configToml.features === "object" && configToml.features !== null) delete configToml.features.codex_hooks;
|
|
6459
7110
|
return smolToml.stringify(configToml);
|
|
6460
7111
|
}
|
|
6461
7112
|
/**
|
|
@@ -6491,6 +7142,12 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
|
|
|
6491
7142
|
relativeFilePath: CODEXCLI_HOOKS_FILE_NAME
|
|
6492
7143
|
};
|
|
6493
7144
|
}
|
|
7145
|
+
static getExtraSharedWritePaths() {
|
|
7146
|
+
return [{
|
|
7147
|
+
relativeDirPath: CODEXCLI_DIR,
|
|
7148
|
+
relativeFilePath: CODEXCLI_MCP_FILE_NAME
|
|
7149
|
+
}];
|
|
7150
|
+
}
|
|
6494
7151
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
6495
7152
|
const paths = CodexcliHooks.getSettablePaths({ global });
|
|
6496
7153
|
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
@@ -6723,6 +7380,20 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
|
|
|
6723
7380
|
}
|
|
6724
7381
|
};
|
|
6725
7382
|
//#endregion
|
|
7383
|
+
//#region src/utils/object.ts
|
|
7384
|
+
/**
|
|
7385
|
+
* Return a shallow copy of `obj` keeping only the entries whose value is
|
|
7386
|
+
* neither `undefined` nor `null`.
|
|
7387
|
+
*
|
|
7388
|
+
* Used to assemble generated config objects without one conditional spread per
|
|
7389
|
+
* optional field (which would otherwise exceed the lint complexity budget).
|
|
7390
|
+
*/
|
|
7391
|
+
function compact(obj) {
|
|
7392
|
+
const result = {};
|
|
7393
|
+
for (const [key, value] of Object.entries(obj)) if (value !== void 0 && value !== null) result[key] = value;
|
|
7394
|
+
return result;
|
|
7395
|
+
}
|
|
7396
|
+
//#endregion
|
|
6726
7397
|
//#region src/features/hooks/copilotcli-hooks.ts
|
|
6727
7398
|
/**
|
|
6728
7399
|
* GitHub Copilot CLI hooks.
|
|
@@ -6734,7 +7405,7 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
|
|
|
6734
7405
|
* (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
|
|
6735
7406
|
* `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
|
|
6736
7407
|
* `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
|
|
6737
|
-
* `notification`). Each entry supports three hook types:
|
|
7408
|
+
* `notification`, `preMcpToolCall`). Each entry supports three hook types:
|
|
6738
7409
|
*
|
|
6739
7410
|
* - `command` — the `bash` / `powershell` command-field shape with optional
|
|
6740
7411
|
* `timeoutSec`, plus optional `cwd` / `env`.
|
|
@@ -6847,20 +7518,24 @@ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchem
|
|
|
6847
7518
|
} else if (hookType === "http") entries.push({
|
|
6848
7519
|
type: "http",
|
|
6849
7520
|
...matcherPart,
|
|
6850
|
-
...
|
|
7521
|
+
...compact({
|
|
7522
|
+
url: def.url,
|
|
7523
|
+
headers: def.headers,
|
|
7524
|
+
allowedEnvVars: def.allowedEnvVars
|
|
7525
|
+
}),
|
|
7526
|
+
...timeoutPart,
|
|
7527
|
+
...rest
|
|
7528
|
+
});
|
|
7529
|
+
else entries.push({
|
|
7530
|
+
type: "command",
|
|
7531
|
+
...matcherPart,
|
|
7532
|
+
...compact({
|
|
7533
|
+
[commandField]: def.command,
|
|
7534
|
+
env: def.env
|
|
7535
|
+
}),
|
|
6851
7536
|
...timeoutPart,
|
|
6852
7537
|
...rest
|
|
6853
7538
|
});
|
|
6854
|
-
else {
|
|
6855
|
-
const command = def.command;
|
|
6856
|
-
entries.push({
|
|
6857
|
-
type: "command",
|
|
6858
|
-
...matcherPart,
|
|
6859
|
-
...command !== void 0 && command !== null && { [commandField]: command },
|
|
6860
|
-
...timeoutPart,
|
|
6861
|
-
...rest
|
|
6862
|
-
});
|
|
6863
|
-
}
|
|
6864
7539
|
}
|
|
6865
7540
|
return entries;
|
|
6866
7541
|
}
|
|
@@ -7269,148 +7944,63 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
|
|
|
7269
7944
|
};
|
|
7270
7945
|
//#endregion
|
|
7271
7946
|
//#region src/features/hooks/devin-hooks.ts
|
|
7947
|
+
const DEVIN_CONVERTER_CONFIG = {
|
|
7948
|
+
supportedEvents: DEVIN_HOOK_EVENTS,
|
|
7949
|
+
canonicalToToolEventNames: CANONICAL_TO_DEVIN_EVENT_NAMES,
|
|
7950
|
+
toolToCanonicalEventNames: DEVIN_TO_CANONICAL_EVENT_NAMES,
|
|
7951
|
+
projectDirVar: "",
|
|
7952
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command", "prompt"]),
|
|
7953
|
+
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
7954
|
+
"sessionStart",
|
|
7955
|
+
"sessionEnd",
|
|
7956
|
+
"stop",
|
|
7957
|
+
"beforeSubmitPrompt"
|
|
7958
|
+
])
|
|
7959
|
+
};
|
|
7272
7960
|
/**
|
|
7273
|
-
*
|
|
7274
|
-
*
|
|
7275
|
-
* The mapping is bijective for every event with a Devin equivalent, so the
|
|
7276
|
-
* conversion round-trips cleanly. Devin splits the generic tool lifecycle
|
|
7277
|
-
* into file/command/MCP specific events, so we line them up with the closest
|
|
7278
|
-
* file/command/MCP specific canonical events rather than the generic
|
|
7279
|
-
* preToolUse/postToolUse pair:
|
|
7280
|
-
*
|
|
7281
|
-
* - beforeReadFile ⇄ pre_read_code
|
|
7282
|
-
* - beforeTabFileRead ⇄ post_read_code
|
|
7283
|
-
* - afterTabFileEdit ⇄ pre_write_code
|
|
7284
|
-
* - afterFileEdit ⇄ post_write_code
|
|
7285
|
-
* - beforeShellExecution⇄ pre_run_command
|
|
7286
|
-
* - afterShellExecution ⇄ post_run_command
|
|
7287
|
-
* - beforeMCPExecution ⇄ pre_mcp_tool_use
|
|
7288
|
-
* - afterMCPExecution ⇄ post_mcp_tool_use
|
|
7289
|
-
* - beforeSubmitPrompt ⇄ pre_user_prompt
|
|
7290
|
-
* - afterAgentResponse ⇄ post_cascade_response
|
|
7291
|
-
* - beforeAgentResponse ⇄ post_cascade_response_with_transcript
|
|
7292
|
-
* - worktreeCreate ⇄ post_setup_worktree
|
|
7293
|
-
*
|
|
7294
|
-
* NOTE on the before/after prefix mismatch: rulesync's canonical vocabulary has
|
|
7295
|
-
* no `afterReadFile` or `beforeWriteFile`/`beforeFileEdit` event — the only read
|
|
7296
|
-
* events are `beforeReadFile`/`beforeTabFileRead` (both read-side) and the only
|
|
7297
|
-
* edit events are `afterFileEdit`/`afterTabFileEdit` (both edit-side). To cover
|
|
7298
|
-
* all twelve Devin events bijectively we therefore pair the second read/write
|
|
7299
|
-
* event by DOMAIN (read↔read, write↔write) rather than by timing, which inverts
|
|
7300
|
-
* the prefix on `post_read_code` (⇄ beforeTabFileRead) and `pre_write_code`
|
|
7301
|
-
* (⇄ afterTabFileEdit). The alternative — pairing by timing — would leave two
|
|
7302
|
-
* Devin events unmapped and drop user hooks. The round-trip stays lossless;
|
|
7303
|
-
* only the human-readable prefix differs, so this is a deliberate trade-off.
|
|
7304
|
-
*
|
|
7305
|
-
* Canonical events that have no Devin equivalent (e.g. sessionStart, stop)
|
|
7306
|
-
* are dropped with a logged warning during export.
|
|
7307
|
-
*/
|
|
7308
|
-
const CANONICAL_TO_DEVIN_EVENT_NAMES = {
|
|
7309
|
-
beforeReadFile: "pre_read_code",
|
|
7310
|
-
beforeTabFileRead: "post_read_code",
|
|
7311
|
-
afterTabFileEdit: "pre_write_code",
|
|
7312
|
-
afterFileEdit: "post_write_code",
|
|
7313
|
-
beforeShellExecution: "pre_run_command",
|
|
7314
|
-
afterShellExecution: "post_run_command",
|
|
7315
|
-
beforeMCPExecution: "pre_mcp_tool_use",
|
|
7316
|
-
afterMCPExecution: "post_mcp_tool_use",
|
|
7317
|
-
beforeSubmitPrompt: "pre_user_prompt",
|
|
7318
|
-
afterAgentResponse: "post_cascade_response",
|
|
7319
|
-
beforeAgentResponse: "post_cascade_response_with_transcript",
|
|
7320
|
-
worktreeCreate: "post_setup_worktree"
|
|
7321
|
-
};
|
|
7322
|
-
/**
|
|
7323
|
-
* Map Devin event names back to canonical camelCase event names.
|
|
7324
|
-
*/
|
|
7325
|
-
const DEVIN_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_DEVIN_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
7326
|
-
/**
|
|
7327
|
-
* Canonical hook events supported by Devin (the keys of the bijective map).
|
|
7961
|
+
* Hooks generator for Devin Local (native `.devin/` hooks).
|
|
7328
7962
|
*
|
|
7329
|
-
*
|
|
7330
|
-
*
|
|
7331
|
-
*
|
|
7332
|
-
*/
|
|
7333
|
-
const DEVIN_HOOK_EVENTS = [
|
|
7334
|
-
"beforeReadFile",
|
|
7335
|
-
"beforeTabFileRead",
|
|
7336
|
-
"afterTabFileEdit",
|
|
7337
|
-
"afterFileEdit",
|
|
7338
|
-
"beforeShellExecution",
|
|
7339
|
-
"afterShellExecution",
|
|
7340
|
-
"beforeMCPExecution",
|
|
7341
|
-
"afterMCPExecution",
|
|
7342
|
-
"beforeSubmitPrompt",
|
|
7343
|
-
"afterAgentResponse",
|
|
7344
|
-
"beforeAgentResponse",
|
|
7345
|
-
"worktreeCreate"
|
|
7346
|
-
];
|
|
7347
|
-
function isRecord(value) {
|
|
7348
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7349
|
-
}
|
|
7350
|
-
/**
|
|
7351
|
-
* Read the per-tool `devin` override hooks from the rulesync hooks config.
|
|
7963
|
+
* Devin Local adopts a Claude-Code-style lifecycle hooks model: each event maps
|
|
7964
|
+
* to an array of `{ matcher?, hooks: [{ type, command|prompt, timeout? }] }`
|
|
7965
|
+
* matcher groups.
|
|
7352
7966
|
*
|
|
7353
|
-
*
|
|
7354
|
-
*
|
|
7355
|
-
* config
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
}
|
|
7360
|
-
/**
|
|
7361
|
-
* Convert a canonical hook definition into a Devin hook object, keeping only
|
|
7362
|
-
* the Devin-supported fields. Returns null when neither command nor
|
|
7363
|
-
* powershell is present (Devin requires at least one of them).
|
|
7364
|
-
*/
|
|
7365
|
-
function canonicalDefToDevinHook(def) {
|
|
7366
|
-
const hook = {};
|
|
7367
|
-
if (typeof def.command === "string") hook.command = def.command;
|
|
7368
|
-
if (typeof def.powershell === "string") hook.powershell = def.powershell;
|
|
7369
|
-
if (typeof def.show_output === "boolean") hook.show_output = def.show_output;
|
|
7370
|
-
if (typeof def.working_directory === "string") hook.working_directory = def.working_directory;
|
|
7371
|
-
if (hook.command === void 0 && hook.powershell === void 0) return null;
|
|
7372
|
-
return hook;
|
|
7373
|
-
}
|
|
7374
|
-
/**
|
|
7375
|
-
* Convert a Devin hook object into a canonical hook definition.
|
|
7376
|
-
*/
|
|
7377
|
-
function devinHookToCanonicalDef(hook) {
|
|
7378
|
-
const def = { type: "command" };
|
|
7379
|
-
if (typeof hook.command === "string") def.command = hook.command;
|
|
7380
|
-
if (typeof hook.powershell === "string") def.powershell = hook.powershell;
|
|
7381
|
-
if (typeof hook.show_output === "boolean") def.show_output = hook.show_output;
|
|
7382
|
-
if (typeof hook.working_directory === "string") def.working_directory = hook.working_directory;
|
|
7383
|
-
return def;
|
|
7384
|
-
}
|
|
7385
|
-
/**
|
|
7386
|
-
* Hooks generator for Devin Cascade (GA).
|
|
7967
|
+
* - Project scope: `.devin/hooks.v1.json`. This is a standalone file whose top
|
|
7968
|
+
* level IS the event map (no wrapper key).
|
|
7969
|
+
* - Global scope: `~/.config/devin/config.json` under the `"hooks"` key. This
|
|
7970
|
+
* file is shared with the MCP (`mcpServers`) and permissions (`permissions`)
|
|
7971
|
+
* features, so reads and writes merge into the existing JSON and the file is
|
|
7972
|
+
* never deleted in global mode.
|
|
7387
7973
|
*
|
|
7388
|
-
*
|
|
7389
|
-
* Devin event name to a flat array of hook objects. Project and global modes
|
|
7390
|
-
* share the same shape; only the location differs (`.windsurf/hooks.json` vs
|
|
7391
|
-
* `~/.codeium/windsurf/hooks.json`). The harness overrides `outputRoot` with the
|
|
7392
|
-
* home directory in global mode.
|
|
7974
|
+
* @see https://docs.devin.ai/cli/extensibility/hooks/overview
|
|
7393
7975
|
*/
|
|
7394
7976
|
var DevinHooks = class DevinHooks extends ToolHooks {
|
|
7395
7977
|
constructor(params) {
|
|
7396
7978
|
super({
|
|
7397
7979
|
...params,
|
|
7398
|
-
fileContent: params.fileContent ??
|
|
7980
|
+
fileContent: params.fileContent ?? "{}"
|
|
7399
7981
|
});
|
|
7400
7982
|
}
|
|
7983
|
+
/**
|
|
7984
|
+
* The project standalone `hooks.v1.json` is owned wholesale by rulesync and
|
|
7985
|
+
* may be deleted as an orphan. The global `config.json` is shared with the MCP
|
|
7986
|
+
* and permissions features, so it must never be deleted.
|
|
7987
|
+
*/
|
|
7988
|
+
isDeletable() {
|
|
7989
|
+
return this.getRelativeFilePath() !== DEVIN_CONFIG_FILE_NAME;
|
|
7990
|
+
}
|
|
7401
7991
|
static getSettablePaths({ global = false } = {}) {
|
|
7402
7992
|
if (global) return {
|
|
7403
|
-
relativeDirPath:
|
|
7404
|
-
relativeFilePath:
|
|
7993
|
+
relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
|
|
7994
|
+
relativeFilePath: DEVIN_CONFIG_FILE_NAME
|
|
7405
7995
|
};
|
|
7406
7996
|
return {
|
|
7407
|
-
relativeDirPath:
|
|
7408
|
-
relativeFilePath:
|
|
7997
|
+
relativeDirPath: DEVIN_DIR,
|
|
7998
|
+
relativeFilePath: DEVIN_HOOKS_V1_FILE_NAME
|
|
7409
7999
|
};
|
|
7410
8000
|
}
|
|
7411
8001
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
7412
8002
|
const paths = DevinHooks.getSettablePaths({ global });
|
|
7413
|
-
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ??
|
|
8003
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
|
|
7414
8004
|
return new DevinHooks({
|
|
7415
8005
|
outputRoot,
|
|
7416
8006
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -7421,31 +8011,32 @@ var DevinHooks = class DevinHooks extends ToolHooks {
|
|
|
7421
8011
|
}
|
|
7422
8012
|
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
7423
8013
|
const paths = DevinHooks.getSettablePaths({ global });
|
|
7424
|
-
|
|
8014
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
7425
8015
|
const config = rulesyncHooks.getJson();
|
|
7426
|
-
const
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
}
|
|
7441
|
-
const objects = [];
|
|
7442
|
-
for (const def of defs) {
|
|
7443
|
-
const hook = canonicalDefToDevinHook(def);
|
|
7444
|
-
if (hook !== null) objects.push(hook);
|
|
8016
|
+
const devinHooks = canonicalToToolHooks({
|
|
8017
|
+
config,
|
|
8018
|
+
toolOverrideHooks: config.devin?.hooks,
|
|
8019
|
+
converterConfig: DEVIN_CONVERTER_CONFIG,
|
|
8020
|
+
logger
|
|
8021
|
+
});
|
|
8022
|
+
let fileContent;
|
|
8023
|
+
if (global) {
|
|
8024
|
+
const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
|
|
8025
|
+
let parsedSettings;
|
|
8026
|
+
try {
|
|
8027
|
+
parsedSettings = JSON.parse(existingContent);
|
|
8028
|
+
} catch (error) {
|
|
8029
|
+
throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
7445
8030
|
}
|
|
7446
|
-
|
|
8031
|
+
const merged = {
|
|
8032
|
+
...isRecord(parsedSettings) ? parsedSettings : {},
|
|
8033
|
+
hooks: devinHooks
|
|
8034
|
+
};
|
|
8035
|
+
fileContent = JSON.stringify(merged, null, 2);
|
|
8036
|
+
} else {
|
|
8037
|
+
await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
|
|
8038
|
+
fileContent = JSON.stringify(devinHooks, null, 2);
|
|
7447
8039
|
}
|
|
7448
|
-
const fileContent = JSON.stringify({ hooks: devinHooks }, null, 2);
|
|
7449
8040
|
return new DevinHooks({
|
|
7450
8041
|
outputRoot,
|
|
7451
8042
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -7461,22 +8052,13 @@ var DevinHooks = class DevinHooks extends ToolHooks {
|
|
|
7461
8052
|
} catch (error) {
|
|
7462
8053
|
throw new Error(`Failed to parse Devin hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
7463
8054
|
}
|
|
7464
|
-
const
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
const canonicalEvent = DEVIN_TO_CANONICAL_EVENT_NAMES[devinEvent];
|
|
7469
|
-
if (canonicalEvent === void 0) continue;
|
|
7470
|
-
const defs = [];
|
|
7471
|
-
for (const rawObject of rawObjects) {
|
|
7472
|
-
if (!isRecord(rawObject)) continue;
|
|
7473
|
-
defs.push(devinHookToCanonicalDef(rawObject));
|
|
7474
|
-
}
|
|
7475
|
-
if (defs.length > 0) canonicalHooks[canonicalEvent] = defs;
|
|
7476
|
-
}
|
|
8055
|
+
const hooks = toolHooksToCanonical({
|
|
8056
|
+
hooks: this.getRelativeFilePath() === "config.json" ? isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {} : parsed,
|
|
8057
|
+
converterConfig: DEVIN_CONVERTER_CONFIG
|
|
8058
|
+
});
|
|
7477
8059
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
|
|
7478
8060
|
version: 1,
|
|
7479
|
-
hooks
|
|
8061
|
+
hooks
|
|
7480
8062
|
}, null, 2) });
|
|
7481
8063
|
}
|
|
7482
8064
|
validate() {
|
|
@@ -7490,7 +8072,7 @@ var DevinHooks = class DevinHooks extends ToolHooks {
|
|
|
7490
8072
|
outputRoot,
|
|
7491
8073
|
relativeDirPath,
|
|
7492
8074
|
relativeFilePath,
|
|
7493
|
-
fileContent:
|
|
8075
|
+
fileContent: "{}",
|
|
7494
8076
|
validate: false
|
|
7495
8077
|
});
|
|
7496
8078
|
}
|
|
@@ -7717,6 +8299,106 @@ function mergeHermesConfig(fileContent, patch) {
|
|
|
7717
8299
|
}
|
|
7718
8300
|
//#endregion
|
|
7719
8301
|
//#region src/features/hooks/hermesagent-hooks.ts
|
|
8302
|
+
/**
|
|
8303
|
+
* Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
|
|
8304
|
+
* `post_tool_call`) and therefore carry a `matcher`. Every other supported
|
|
8305
|
+
* canonical event maps to a Hermes lifecycle event, which never accepts a
|
|
8306
|
+
* `matcher`.
|
|
8307
|
+
* @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
|
|
8308
|
+
*/
|
|
8309
|
+
const HERMESAGENT_MATCHER_EVENTS = /* @__PURE__ */ new Set(["preToolUse", "postToolUse"]);
|
|
8310
|
+
/**
|
|
8311
|
+
* Filter the shared canonical hooks to the events Hermes understands and merge
|
|
8312
|
+
* the `hermesagent`-specific override block on top.
|
|
8313
|
+
*/
|
|
8314
|
+
function buildEffectiveHooks(config, toolOverrideHooks) {
|
|
8315
|
+
const supported = new Set(HERMESAGENT_HOOK_EVENTS);
|
|
8316
|
+
const shared = {};
|
|
8317
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (supported.has(event)) shared[event] = defs;
|
|
8318
|
+
return {
|
|
8319
|
+
...shared,
|
|
8320
|
+
...toolOverrideHooks
|
|
8321
|
+
};
|
|
8322
|
+
}
|
|
8323
|
+
/**
|
|
8324
|
+
* Convert the canonical hooks config into Hermes's native
|
|
8325
|
+
* `hooks: { <event>: [{ matcher?, command, timeout? }] }` shape.
|
|
8326
|
+
*
|
|
8327
|
+
* Only `type: "command"` canonical hooks are emitted — Hermes shell hooks run
|
|
8328
|
+
* via `shlex.split`/`shell=False`, so `prompt`/`http` hooks have no native
|
|
8329
|
+
* equivalent and are skipped (the shared `HooksProcessor` already warns about
|
|
8330
|
+
* unsupported hook types centrally). `matcher` is only carried through for
|
|
8331
|
+
* `pre_tool_call`/`post_tool_call`; on any other event it is dropped with a
|
|
8332
|
+
* warning, mirroring how other adapters (e.g. AugmentCode) handle
|
|
8333
|
+
* matcher-less lifecycle events.
|
|
8334
|
+
*/
|
|
8335
|
+
function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
|
|
8336
|
+
const effectiveHooks = buildEffectiveHooks(config, toolOverrideHooks);
|
|
8337
|
+
const result = {};
|
|
8338
|
+
for (const [canonicalEvent, defs] of Object.entries(effectiveHooks)) {
|
|
8339
|
+
const nativeEvent = CANONICAL_TO_HERMESAGENT_EVENT_NAMES[canonicalEvent];
|
|
8340
|
+
if (!nativeEvent) continue;
|
|
8341
|
+
const supportsMatcher = HERMESAGENT_MATCHER_EVENTS.has(canonicalEvent);
|
|
8342
|
+
const entries = [];
|
|
8343
|
+
for (const def of defs) {
|
|
8344
|
+
if ((def.type ?? "command") !== "command") continue;
|
|
8345
|
+
if (typeof def.command !== "string" || def.command === "") continue;
|
|
8346
|
+
const entry = { command: def.command };
|
|
8347
|
+
if (typeof def.matcher === "string" && def.matcher !== "") if (supportsMatcher) entry.matcher = def.matcher;
|
|
8348
|
+
else logger?.warn(`matcher "${def.matcher}" on "${canonicalEvent}" hook will be ignored — Hermes Agent only supports matchers on pre_tool_call/post_tool_call`);
|
|
8349
|
+
if (typeof def.timeout === "number") entry.timeout = def.timeout;
|
|
8350
|
+
entries.push(entry);
|
|
8351
|
+
}
|
|
8352
|
+
if (entries.length > 0) result[nativeEvent] = entries;
|
|
8353
|
+
}
|
|
8354
|
+
return result;
|
|
8355
|
+
}
|
|
8356
|
+
/**
|
|
8357
|
+
* Reverse {@link canonicalToHermesHooks}: parse Hermes's native
|
|
8358
|
+
* `hooks: { <event>: [...] }` map back into a canonical event → definition[]
|
|
8359
|
+
* record. Native events with no canonical equivalent (`pre_verify`,
|
|
8360
|
+
* `transform_tool_result`, ...) are skipped since there is nothing to round
|
|
8361
|
+
* -trip them into.
|
|
8362
|
+
*/
|
|
8363
|
+
function hermesHooksToCanonical(hooks) {
|
|
8364
|
+
const canonical = {};
|
|
8365
|
+
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
|
|
8366
|
+
for (const [nativeEvent, entries] of Object.entries(hooks)) {
|
|
8367
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(nativeEvent) || !Array.isArray(entries)) continue;
|
|
8368
|
+
const canonicalEvent = HERMESAGENT_TO_CANONICAL_EVENT_NAMES[nativeEvent];
|
|
8369
|
+
if (!canonicalEvent) continue;
|
|
8370
|
+
const defs = [];
|
|
8371
|
+
for (const raw of entries) {
|
|
8372
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
8373
|
+
const entry = raw;
|
|
8374
|
+
if (typeof entry.command !== "string") continue;
|
|
8375
|
+
const def = {
|
|
8376
|
+
type: "command",
|
|
8377
|
+
command: entry.command
|
|
8378
|
+
};
|
|
8379
|
+
if (typeof entry.matcher === "string" && entry.matcher !== "") def.matcher = entry.matcher;
|
|
8380
|
+
if (typeof entry.timeout === "number") def.timeout = entry.timeout;
|
|
8381
|
+
defs.push(def);
|
|
8382
|
+
}
|
|
8383
|
+
if (defs.length > 0) canonical[canonicalEvent] = defs;
|
|
8384
|
+
}
|
|
8385
|
+
return canonical;
|
|
8386
|
+
}
|
|
8387
|
+
/**
|
|
8388
|
+
* Hermes Agent shell hooks.
|
|
8389
|
+
*
|
|
8390
|
+
* Hermes Agent registers shell-command hooks under the `hooks:` key of the
|
|
8391
|
+
* shared user config file `~/.hermes/config.yaml` (the HERMES_HOME directory;
|
|
8392
|
+
* global only — Hermes has no project-scoped hooks location). Hermes only runs
|
|
8393
|
+
* hooks declared under its fixed `VALID_HOOKS` event keys (`pre_tool_call`,
|
|
8394
|
+
* `post_tool_call`, `pre_llm_call`, `post_llm_call`, `on_session_start`,
|
|
8395
|
+
* `on_session_end`, `subagent_start`, `subagent_stop`, ...); any other key is
|
|
8396
|
+
* silently ignored. Generation therefore maps canonical events onto the real
|
|
8397
|
+
* `VALID_HOOKS` keys and merges the resulting `hooks:` block into the existing
|
|
8398
|
+
* config instead of overwriting it, since that file also holds other Hermes
|
|
8399
|
+
* settings (model, `mcp_servers`, `command_allowlist`, ...).
|
|
8400
|
+
* @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
|
|
8401
|
+
*/
|
|
7720
8402
|
var HermesagentHooks = class HermesagentHooks extends ToolHooks {
|
|
7721
8403
|
static getSettablePaths() {
|
|
7722
8404
|
return {
|
|
@@ -7736,6 +8418,9 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
|
|
|
7736
8418
|
error: null
|
|
7737
8419
|
};
|
|
7738
8420
|
}
|
|
8421
|
+
isDeletable() {
|
|
8422
|
+
return false;
|
|
8423
|
+
}
|
|
7739
8424
|
shouldMergeExistingFileContent() {
|
|
7740
8425
|
return true;
|
|
7741
8426
|
}
|
|
@@ -7743,18 +8428,21 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
|
|
|
7743
8428
|
this.fileContent = mergeHermesConfig(fileContent, parseHermesConfig(this.fileContent));
|
|
7744
8429
|
}
|
|
7745
8430
|
toRulesyncHooks() {
|
|
7746
|
-
const
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
fileContent: JSON.stringify(hooks ?? {}, null, 2)
|
|
7752
|
-
});
|
|
8431
|
+
const hooks = hermesHooksToCanonical(parseHermesConfig(this.getFileContent()).hooks);
|
|
8432
|
+
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
|
|
8433
|
+
version: 1,
|
|
8434
|
+
hooks
|
|
8435
|
+
}, null, 2) });
|
|
7753
8436
|
}
|
|
7754
|
-
static fromRulesyncHooks({ outputRoot, rulesyncHooks }) {
|
|
8437
|
+
static fromRulesyncHooks({ outputRoot, rulesyncHooks, logger }) {
|
|
8438
|
+
const config = rulesyncHooks.getJson();
|
|
7755
8439
|
return new HermesagentHooks({
|
|
7756
8440
|
outputRoot,
|
|
7757
|
-
fileContent: stringifyHermesConfig({ hooks: {
|
|
8441
|
+
fileContent: stringifyHermesConfig({ hooks: canonicalToHermesHooks({
|
|
8442
|
+
config,
|
|
8443
|
+
toolOverrideHooks: config.hermesagent?.hooks,
|
|
8444
|
+
logger
|
|
8445
|
+
}) })
|
|
7758
8446
|
});
|
|
7759
8447
|
}
|
|
7760
8448
|
};
|
|
@@ -8224,9 +8912,8 @@ var KiroHooks = class KiroHooks extends ToolHooks {
|
|
|
8224
8912
|
* the tool-specific override key to `kiro-cli` (so `kiro-cli.hooks` overrides in
|
|
8225
8913
|
* the rulesync hooks config are honored, rather than the legacy `kiro.hooks`).
|
|
8226
8914
|
*
|
|
8227
|
-
* (The Kiro IDE uses
|
|
8228
|
-
*
|
|
8229
|
-
* adapter.)
|
|
8915
|
+
* (The Kiro IDE uses the structured `.kiro/hooks/*.json` v1 format instead; see
|
|
8916
|
+
* {@link import("./kiro-ide-hooks.js").KiroIdeHooks}.)
|
|
8230
8917
|
*/
|
|
8231
8918
|
var KiroCliHooks = class extends KiroHooks {
|
|
8232
8919
|
static getOverrideKey() {
|
|
@@ -8234,6 +8921,203 @@ var KiroCliHooks = class extends KiroHooks {
|
|
|
8234
8921
|
}
|
|
8235
8922
|
};
|
|
8236
8923
|
//#endregion
|
|
8924
|
+
//#region src/features/hooks/kiro-ide-hooks.ts
|
|
8925
|
+
/**
|
|
8926
|
+
* One hook entry inside the Kiro IDE v1 `hooks` array.
|
|
8927
|
+
*
|
|
8928
|
+
* `z.looseObject` keeps unknown fields added by future Kiro IDE versions, so
|
|
8929
|
+
* imports do not drop data they do not yet understand.
|
|
8930
|
+
* @see https://kiro.dev/docs/hooks/types/
|
|
8931
|
+
*/
|
|
8932
|
+
const KiroIdeHookActionSchema = z.union([z.looseObject({
|
|
8933
|
+
type: z.literal("command"),
|
|
8934
|
+
command: z.optional(safeString)
|
|
8935
|
+
}), z.looseObject({
|
|
8936
|
+
type: z.literal("agent"),
|
|
8937
|
+
prompt: z.optional(safeString)
|
|
8938
|
+
})]);
|
|
8939
|
+
const KiroIdeHookEntrySchema = z.looseObject({
|
|
8940
|
+
name: z.optional(z.string()),
|
|
8941
|
+
description: z.optional(z.string()),
|
|
8942
|
+
trigger: z.optional(z.string()),
|
|
8943
|
+
matcher: z.optional(z.string()),
|
|
8944
|
+
action: z.optional(KiroIdeHookActionSchema),
|
|
8945
|
+
timeout: z.optional(z.number()),
|
|
8946
|
+
enabled: z.optional(z.boolean())
|
|
8947
|
+
});
|
|
8948
|
+
const KiroIdeHooksFileSchema = z.looseObject({
|
|
8949
|
+
version: z.optional(z.string()),
|
|
8950
|
+
hooks: z.optional(z.array(KiroIdeHookEntrySchema))
|
|
8951
|
+
});
|
|
8952
|
+
/**
|
|
8953
|
+
* Build the Kiro IDE hook entries for a single canonical event's definitions.
|
|
8954
|
+
*
|
|
8955
|
+
* `command`-type definitions become `{ type: "command", command }` actions and
|
|
8956
|
+
* `prompt`-type definitions become `{ type: "agent", prompt }` actions. Other
|
|
8957
|
+
* types are skipped (the {@link import("./hooks-processor.js").HooksProcessor}
|
|
8958
|
+
* already warns about unsupported types).
|
|
8959
|
+
*/
|
|
8960
|
+
function buildKiroIdeEntriesForEvent(trigger, definitions) {
|
|
8961
|
+
const entries = [];
|
|
8962
|
+
for (const def of definitions) {
|
|
8963
|
+
const type = def.type ?? "command";
|
|
8964
|
+
let action;
|
|
8965
|
+
if (type === "command") {
|
|
8966
|
+
if (def.command === void 0) continue;
|
|
8967
|
+
action = {
|
|
8968
|
+
type: "command",
|
|
8969
|
+
command: def.command
|
|
8970
|
+
};
|
|
8971
|
+
} else if (type === "prompt") {
|
|
8972
|
+
if (def.prompt === void 0) continue;
|
|
8973
|
+
action = {
|
|
8974
|
+
type: "agent",
|
|
8975
|
+
prompt: def.prompt
|
|
8976
|
+
};
|
|
8977
|
+
} else continue;
|
|
8978
|
+
entries.push({
|
|
8979
|
+
name: def.name ?? trigger,
|
|
8980
|
+
...def.description !== void 0 && def.description !== null && { description: def.description },
|
|
8981
|
+
trigger,
|
|
8982
|
+
...def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" && { matcher: def.matcher },
|
|
8983
|
+
action,
|
|
8984
|
+
...def.timeout !== void 0 && def.timeout !== null && def.timeout >= 0 && { timeout: def.timeout },
|
|
8985
|
+
enabled: true
|
|
8986
|
+
});
|
|
8987
|
+
}
|
|
8988
|
+
return entries;
|
|
8989
|
+
}
|
|
8990
|
+
function canonicalToKiroIdeHooks(config) {
|
|
8991
|
+
const kiroIdeSupported = new Set(KIRO_IDE_HOOK_EVENTS);
|
|
8992
|
+
const sharedHooks = {};
|
|
8993
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (kiroIdeSupported.has(event)) sharedHooks[event] = defs;
|
|
8994
|
+
const effectiveHooks = {
|
|
8995
|
+
...sharedHooks,
|
|
8996
|
+
...config["kiro-ide"]?.hooks
|
|
8997
|
+
};
|
|
8998
|
+
const entries = [];
|
|
8999
|
+
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
9000
|
+
const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? eventName;
|
|
9001
|
+
entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
|
|
9002
|
+
}
|
|
9003
|
+
return entries;
|
|
9004
|
+
}
|
|
9005
|
+
function kiroIdeHooksToCanonical(entries) {
|
|
9006
|
+
const canonical = {};
|
|
9007
|
+
for (const entry of entries) {
|
|
9008
|
+
if (entry.trigger === void 0 || entry.action === void 0) continue;
|
|
9009
|
+
const eventName = KIRO_IDE_TO_CANONICAL_EVENT_NAMES[entry.trigger] ?? entry.trigger;
|
|
9010
|
+
if (isPrototypePollutionKey(eventName)) continue;
|
|
9011
|
+
const def = {};
|
|
9012
|
+
if (entry.action.type === "command") {
|
|
9013
|
+
if (!entry.action.command) continue;
|
|
9014
|
+
def.type = "command";
|
|
9015
|
+
def.command = entry.action.command;
|
|
9016
|
+
} else {
|
|
9017
|
+
if (!entry.action.prompt) continue;
|
|
9018
|
+
def.type = "prompt";
|
|
9019
|
+
def.prompt = entry.action.prompt;
|
|
9020
|
+
}
|
|
9021
|
+
if (entry.name !== void 0 && entry.name !== null) def.name = entry.name;
|
|
9022
|
+
if (entry.description !== void 0 && entry.description !== null) def.description = entry.description;
|
|
9023
|
+
if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
|
|
9024
|
+
if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
|
|
9025
|
+
(canonical[eventName] ??= []).push(def);
|
|
9026
|
+
}
|
|
9027
|
+
return canonical;
|
|
9028
|
+
}
|
|
9029
|
+
/**
|
|
9030
|
+
* Hooks generator for the **Kiro IDE** (`.kiro/hooks/*.json` v1).
|
|
9031
|
+
*
|
|
9032
|
+
* Kiro IDE 1.0 reads structured JSON hooks from `.kiro/hooks/` (workspace) and
|
|
9033
|
+
* `~/.kiro/hooks/` (user). A single file may declare multiple hooks in its
|
|
9034
|
+
* `hooks` array, so rulesync emits every generated hook into one
|
|
9035
|
+
* `rulesync.json` file per scope (`{ "version": "v1", "hooks": [ ... ] }`),
|
|
9036
|
+
* which keeps it within the single-file hooks architecture.
|
|
9037
|
+
*
|
|
9038
|
+
* This is distinct from the Kiro CLI ({@link import("./kiro-cli-hooks.js").
|
|
9039
|
+
* KiroCliHooks}), which uses the `.kiro/agents/default.json` agent-config shape.
|
|
9040
|
+
*
|
|
9041
|
+
* @see https://kiro.dev/docs/hooks/
|
|
9042
|
+
*/
|
|
9043
|
+
var KiroIdeHooks = class KiroIdeHooks extends ToolHooks {
|
|
9044
|
+
constructor(params) {
|
|
9045
|
+
super({
|
|
9046
|
+
...params,
|
|
9047
|
+
fileContent: params.fileContent ?? JSON.stringify({
|
|
9048
|
+
version: "v1",
|
|
9049
|
+
hooks: []
|
|
9050
|
+
}, null, 2)
|
|
9051
|
+
});
|
|
9052
|
+
}
|
|
9053
|
+
static getSettablePaths(_options = {}) {
|
|
9054
|
+
return {
|
|
9055
|
+
relativeDirPath: KIRO_IDE_HOOKS_DIR_PATH,
|
|
9056
|
+
relativeFilePath: KIRO_IDE_HOOKS_FILE_NAME
|
|
9057
|
+
};
|
|
9058
|
+
}
|
|
9059
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
9060
|
+
const paths = KiroIdeHooks.getSettablePaths({ global });
|
|
9061
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({
|
|
9062
|
+
version: "v1",
|
|
9063
|
+
hooks: []
|
|
9064
|
+
}, null, 2);
|
|
9065
|
+
return new KiroIdeHooks({
|
|
9066
|
+
outputRoot,
|
|
9067
|
+
relativeDirPath: paths.relativeDirPath,
|
|
9068
|
+
relativeFilePath: paths.relativeFilePath,
|
|
9069
|
+
fileContent,
|
|
9070
|
+
validate
|
|
9071
|
+
});
|
|
9072
|
+
}
|
|
9073
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
|
|
9074
|
+
const paths = KiroIdeHooks.getSettablePaths({ global });
|
|
9075
|
+
const hooks = canonicalToKiroIdeHooks(rulesyncHooks.getJson());
|
|
9076
|
+
const fileContent = JSON.stringify({
|
|
9077
|
+
version: "v1",
|
|
9078
|
+
hooks
|
|
9079
|
+
}, null, 2);
|
|
9080
|
+
return new KiroIdeHooks({
|
|
9081
|
+
outputRoot,
|
|
9082
|
+
relativeDirPath: paths.relativeDirPath,
|
|
9083
|
+
relativeFilePath: paths.relativeFilePath,
|
|
9084
|
+
fileContent,
|
|
9085
|
+
validate
|
|
9086
|
+
});
|
|
9087
|
+
}
|
|
9088
|
+
toRulesyncHooks() {
|
|
9089
|
+
let parsed;
|
|
9090
|
+
try {
|
|
9091
|
+
parsed = KiroIdeHooksFileSchema.parse(JSON.parse(this.getFileContent()));
|
|
9092
|
+
} catch (error) {
|
|
9093
|
+
throw new Error(`Failed to parse Kiro IDE hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
9094
|
+
}
|
|
9095
|
+
const hooks = kiroIdeHooksToCanonical(parsed.hooks ?? []);
|
|
9096
|
+
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
|
|
9097
|
+
version: 1,
|
|
9098
|
+
hooks
|
|
9099
|
+
}, null, 2) });
|
|
9100
|
+
}
|
|
9101
|
+
validate() {
|
|
9102
|
+
return {
|
|
9103
|
+
success: true,
|
|
9104
|
+
error: null
|
|
9105
|
+
};
|
|
9106
|
+
}
|
|
9107
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
9108
|
+
return new KiroIdeHooks({
|
|
9109
|
+
outputRoot,
|
|
9110
|
+
relativeDirPath,
|
|
9111
|
+
relativeFilePath,
|
|
9112
|
+
fileContent: JSON.stringify({
|
|
9113
|
+
version: "v1",
|
|
9114
|
+
hooks: []
|
|
9115
|
+
}, null, 2),
|
|
9116
|
+
validate: false
|
|
9117
|
+
});
|
|
9118
|
+
}
|
|
9119
|
+
};
|
|
9120
|
+
//#endregion
|
|
8237
9121
|
//#region src/features/hooks/opencode-hooks.ts
|
|
8238
9122
|
var OpencodeHooks = class OpencodeHooks extends ToolHooks {
|
|
8239
9123
|
constructor(params) {
|
|
@@ -8292,6 +9176,35 @@ var OpencodeHooks = class OpencodeHooks extends ToolHooks {
|
|
|
8292
9176
|
//#endregion
|
|
8293
9177
|
//#region src/features/hooks/qwencode-hooks.ts
|
|
8294
9178
|
/**
|
|
9179
|
+
* Build a single Qwen Code hook object from a canonical hook definition.
|
|
9180
|
+
* Command-only fields (`async`/`env`/`shell`) are emitted only on command hooks
|
|
9181
|
+
* and http-only fields (`headers`/`allowedEnvVars`/`once`) only on http hooks,
|
|
9182
|
+
* matching upstream. `statusMessage` applies to both.
|
|
9183
|
+
* https://github.com/QwenLM/qwen-code/blob/main/docs/users/features/hooks.md
|
|
9184
|
+
*/
|
|
9185
|
+
function canonicalDefToQwencodeHook(def) {
|
|
9186
|
+
const type = def.type ?? "command";
|
|
9187
|
+
const isHttp = type === "http";
|
|
9188
|
+
const isCommand = type === "command";
|
|
9189
|
+
return {
|
|
9190
|
+
type,
|
|
9191
|
+
...compact({
|
|
9192
|
+
command: def.command,
|
|
9193
|
+
url: def.url,
|
|
9194
|
+
timeout: def.timeout,
|
|
9195
|
+
name: def.name,
|
|
9196
|
+
description: def.description,
|
|
9197
|
+
statusMessage: def.statusMessage,
|
|
9198
|
+
async: isCommand ? def.async : void 0,
|
|
9199
|
+
env: isCommand ? def.env : void 0,
|
|
9200
|
+
shell: isCommand ? def.shell : void 0,
|
|
9201
|
+
headers: isHttp ? def.headers : void 0,
|
|
9202
|
+
allowedEnvVars: isHttp ? def.allowedEnvVars : void 0,
|
|
9203
|
+
once: isHttp ? def.once : void 0
|
|
9204
|
+
})
|
|
9205
|
+
};
|
|
9206
|
+
}
|
|
9207
|
+
/**
|
|
8295
9208
|
* Convert canonical hooks config to Qwen Code format.
|
|
8296
9209
|
* Filters shared hooks to QWENCODE_HOOK_EVENTS, merges config.qwencode?.hooks,
|
|
8297
9210
|
* then converts to PascalCase and Qwen Code matcher/hooks structure.
|
|
@@ -8319,16 +9232,7 @@ function canonicalToQwencodeHooks(config) {
|
|
|
8319
9232
|
}
|
|
8320
9233
|
const entries = [];
|
|
8321
9234
|
for (const [matcherKey, defs] of byMatcher) {
|
|
8322
|
-
const hooks = defs.map(
|
|
8323
|
-
return {
|
|
8324
|
-
type: def.type ?? "command",
|
|
8325
|
-
...def.command !== void 0 && def.command !== null && { command: def.command },
|
|
8326
|
-
...def.url !== void 0 && def.url !== null && { url: def.url },
|
|
8327
|
-
...def.timeout !== void 0 && def.timeout !== null && { timeout: def.timeout },
|
|
8328
|
-
...def.name !== void 0 && def.name !== null && { name: def.name },
|
|
8329
|
-
...def.description !== void 0 && def.description !== null && { description: def.description }
|
|
8330
|
-
};
|
|
8331
|
-
});
|
|
9235
|
+
const hooks = defs.map(canonicalDefToQwencodeHook);
|
|
8332
9236
|
const sequential = defs.some((def) => def.sequential === true);
|
|
8333
9237
|
const group = matcherKey ? {
|
|
8334
9238
|
matcher: matcherKey,
|
|
@@ -8352,7 +9256,14 @@ const QwencodeHookEntrySchema = z.looseObject({
|
|
|
8352
9256
|
url: z.optional(z.string()),
|
|
8353
9257
|
timeout: z.optional(z.number()),
|
|
8354
9258
|
name: z.optional(z.string()),
|
|
8355
|
-
description: z.optional(z.string())
|
|
9259
|
+
description: z.optional(z.string()),
|
|
9260
|
+
statusMessage: z.optional(z.string()),
|
|
9261
|
+
async: z.optional(z.boolean()),
|
|
9262
|
+
env: z.optional(z.record(z.string(), z.string())),
|
|
9263
|
+
shell: z.optional(z.string()),
|
|
9264
|
+
headers: z.optional(z.record(z.string(), z.string())),
|
|
9265
|
+
allowedEnvVars: z.optional(z.array(z.string())),
|
|
9266
|
+
once: z.optional(z.boolean())
|
|
8356
9267
|
});
|
|
8357
9268
|
/**
|
|
8358
9269
|
* A matcher group entry in a Qwen Code event array.
|
|
@@ -8371,18 +9282,29 @@ function qwencodeMatcherEntryToCanonical(entry) {
|
|
|
8371
9282
|
const defs = [];
|
|
8372
9283
|
const hooks = entry.hooks ?? [];
|
|
8373
9284
|
const sequential = entry.sequential === true;
|
|
9285
|
+
const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" ? entry.matcher : void 0;
|
|
8374
9286
|
for (const h of hooks) {
|
|
8375
|
-
const command = h.command;
|
|
8376
9287
|
const hookType = h.type === "command" || h.type === "prompt" || h.type === "http" ? h.type : "command";
|
|
9288
|
+
const isHttp = hookType === "http";
|
|
9289
|
+
const isCommand = hookType === "command";
|
|
8377
9290
|
defs.push({
|
|
8378
9291
|
type: hookType,
|
|
8379
|
-
...
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
9292
|
+
...compact({
|
|
9293
|
+
command: h.command,
|
|
9294
|
+
url: h.url,
|
|
9295
|
+
timeout: h.timeout,
|
|
9296
|
+
name: h.name,
|
|
9297
|
+
description: h.description,
|
|
9298
|
+
statusMessage: h.statusMessage,
|
|
9299
|
+
async: isCommand ? h.async : void 0,
|
|
9300
|
+
env: isCommand ? h.env : void 0,
|
|
9301
|
+
shell: isCommand ? h.shell : void 0,
|
|
9302
|
+
headers: isHttp ? h.headers : void 0,
|
|
9303
|
+
allowedEnvVars: isHttp ? h.allowedEnvVars : void 0,
|
|
9304
|
+
once: isHttp ? h.once : void 0,
|
|
9305
|
+
sequential: sequential ? true : void 0,
|
|
9306
|
+
matcher
|
|
9307
|
+
})
|
|
8386
9308
|
});
|
|
8387
9309
|
}
|
|
8388
9310
|
return defs;
|
|
@@ -8495,6 +9417,169 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
|
|
|
8495
9417
|
}
|
|
8496
9418
|
};
|
|
8497
9419
|
//#endregion
|
|
9420
|
+
//#region src/features/hooks/reasonix-hooks.ts
|
|
9421
|
+
/**
|
|
9422
|
+
* Only PreToolUse/PostToolUse honor the `match` field (an anchored regex
|
|
9423
|
+
* against the tool name); it is ignored on every other event.
|
|
9424
|
+
*/
|
|
9425
|
+
const REASONIX_MATCHER_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse"]);
|
|
9426
|
+
const SUPPORTED_REASONIX_EVENTS = new Set(REASONIX_HOOK_EVENTS);
|
|
9427
|
+
/**
|
|
9428
|
+
* Convert canonical hooks config to the Reasonix `hooks` object.
|
|
9429
|
+
* Filters shared hooks to REASONIX_HOOK_EVENTS, merges `config.reasonix?.hooks`,
|
|
9430
|
+
* then maps event names and emits flat per-event hook-entry arrays.
|
|
9431
|
+
*/
|
|
9432
|
+
function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
|
|
9433
|
+
const sharedHooks = {};
|
|
9434
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_REASONIX_EVENTS.has(event)) sharedHooks[event] = defs;
|
|
9435
|
+
const effectiveHooks = {
|
|
9436
|
+
...sharedHooks,
|
|
9437
|
+
...toolOverrideHooks
|
|
9438
|
+
};
|
|
9439
|
+
const result = {};
|
|
9440
|
+
for (const [event, defs] of Object.entries(effectiveHooks)) {
|
|
9441
|
+
if (!SUPPORTED_REASONIX_EVENTS.has(event)) continue;
|
|
9442
|
+
const reasonixEvent = CANONICAL_TO_REASONIX_EVENT_NAMES[event] ?? event;
|
|
9443
|
+
const isMatcherEvent = REASONIX_MATCHER_EVENTS.has(reasonixEvent);
|
|
9444
|
+
const entries = [];
|
|
9445
|
+
for (const def of defs) {
|
|
9446
|
+
if ((def.type ?? "command") !== "command") continue;
|
|
9447
|
+
if (typeof def.command !== "string") continue;
|
|
9448
|
+
const entry = { command: def.command };
|
|
9449
|
+
if (typeof def.matcher === "string" && def.matcher !== "") if (isMatcherEvent) entry.match = def.matcher;
|
|
9450
|
+
else logger?.warn(`matcher "${def.matcher}" on "${event}" hook will be ignored — Reasonix's "${reasonixEvent}" event does not support matchers`);
|
|
9451
|
+
if (typeof def.description === "string" && def.description !== "") entry.description = def.description;
|
|
9452
|
+
if (typeof def.timeout === "number") entry.timeout = Math.round(def.timeout * 1e3);
|
|
9453
|
+
entries.push(entry);
|
|
9454
|
+
}
|
|
9455
|
+
if (entries.length > 0) result[reasonixEvent] = [...result[reasonixEvent] ?? [], ...entries];
|
|
9456
|
+
}
|
|
9457
|
+
return result;
|
|
9458
|
+
}
|
|
9459
|
+
/**
|
|
9460
|
+
* Reverse {@link canonicalToReasonixHooks}: parse the Reasonix `hooks` object
|
|
9461
|
+
* back into a canonical event -> definition[] record.
|
|
9462
|
+
*/
|
|
9463
|
+
function reasonixHooksToCanonical(hooks) {
|
|
9464
|
+
const canonical = {};
|
|
9465
|
+
if (hooks === null || hooks === void 0 || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
|
|
9466
|
+
for (const [reasonixEvent, rawEntries] of Object.entries(hooks)) {
|
|
9467
|
+
if (!Array.isArray(rawEntries)) continue;
|
|
9468
|
+
const canonicalEvent = REASONIX_TO_CANONICAL_EVENT_NAMES[reasonixEvent] ?? reasonixEvent;
|
|
9469
|
+
const defs = [];
|
|
9470
|
+
for (const rawEntry of rawEntries) {
|
|
9471
|
+
if (rawEntry === null || typeof rawEntry !== "object" || Array.isArray(rawEntry)) continue;
|
|
9472
|
+
const entry = rawEntry;
|
|
9473
|
+
if (typeof entry.command !== "string") continue;
|
|
9474
|
+
const def = {
|
|
9475
|
+
type: "command",
|
|
9476
|
+
command: entry.command
|
|
9477
|
+
};
|
|
9478
|
+
if (typeof entry.match === "string" && entry.match !== "") def.matcher = entry.match;
|
|
9479
|
+
if (typeof entry.description === "string" && entry.description !== "") def.description = entry.description;
|
|
9480
|
+
if (typeof entry.timeout === "number") def.timeout = entry.timeout / 1e3;
|
|
9481
|
+
defs.push(def);
|
|
9482
|
+
}
|
|
9483
|
+
if (defs.length > 0) canonical[canonicalEvent] = [...canonical[canonicalEvent] ?? [], ...defs];
|
|
9484
|
+
}
|
|
9485
|
+
return canonical;
|
|
9486
|
+
}
|
|
9487
|
+
/**
|
|
9488
|
+
* Reasonix hooks adapter.
|
|
9489
|
+
*
|
|
9490
|
+
* Reasonix hooks live in a Claude-Code-style but standalone JSON file —
|
|
9491
|
+
* `.reasonix/settings.json` (project) or `~/.reasonix/settings.json`
|
|
9492
|
+
* (global) — separate from the `[permissions]`/`[[plugins]]` TOML config.
|
|
9493
|
+
* Only the four events documented in the upstream issue are mapped:
|
|
9494
|
+
* PreToolUse/PostToolUse/UserPromptSubmit/Stop (see REASONIX_HOOK_EVENTS).
|
|
9495
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
|
|
9496
|
+
*/
|
|
9497
|
+
var ReasonixHooks = class ReasonixHooks extends ToolHooks {
|
|
9498
|
+
constructor(params) {
|
|
9499
|
+
super({
|
|
9500
|
+
...params,
|
|
9501
|
+
fileContent: params.fileContent ?? "{}"
|
|
9502
|
+
});
|
|
9503
|
+
}
|
|
9504
|
+
isDeletable() {
|
|
9505
|
+
return false;
|
|
9506
|
+
}
|
|
9507
|
+
static getSettablePaths(_options = {}) {
|
|
9508
|
+
return {
|
|
9509
|
+
relativeDirPath: REASONIX_DIR,
|
|
9510
|
+
relativeFilePath: REASONIX_SETTINGS_FILE_NAME
|
|
9511
|
+
};
|
|
9512
|
+
}
|
|
9513
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
9514
|
+
const paths = ReasonixHooks.getSettablePaths({ global });
|
|
9515
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
9516
|
+
return new ReasonixHooks({
|
|
9517
|
+
outputRoot,
|
|
9518
|
+
relativeDirPath: paths.relativeDirPath,
|
|
9519
|
+
relativeFilePath: paths.relativeFilePath,
|
|
9520
|
+
fileContent,
|
|
9521
|
+
validate
|
|
9522
|
+
});
|
|
9523
|
+
}
|
|
9524
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
9525
|
+
const paths = ReasonixHooks.getSettablePaths({ global });
|
|
9526
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
9527
|
+
const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
|
|
9528
|
+
let settings;
|
|
9529
|
+
try {
|
|
9530
|
+
settings = JSON.parse(existingContent);
|
|
9531
|
+
} catch (error) {
|
|
9532
|
+
throw new Error(`Failed to parse existing Reasonix settings at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
9533
|
+
}
|
|
9534
|
+
const config = rulesyncHooks.getJson();
|
|
9535
|
+
const reasonixHooks = canonicalToReasonixHooks({
|
|
9536
|
+
config,
|
|
9537
|
+
toolOverrideHooks: config.reasonix?.hooks,
|
|
9538
|
+
logger
|
|
9539
|
+
});
|
|
9540
|
+
const merged = {
|
|
9541
|
+
...settings,
|
|
9542
|
+
hooks: reasonixHooks
|
|
9543
|
+
};
|
|
9544
|
+
const fileContent = JSON.stringify(merged, null, 2);
|
|
9545
|
+
return new ReasonixHooks({
|
|
9546
|
+
outputRoot,
|
|
9547
|
+
relativeDirPath: paths.relativeDirPath,
|
|
9548
|
+
relativeFilePath: paths.relativeFilePath,
|
|
9549
|
+
fileContent,
|
|
9550
|
+
validate
|
|
9551
|
+
});
|
|
9552
|
+
}
|
|
9553
|
+
toRulesyncHooks() {
|
|
9554
|
+
let settings;
|
|
9555
|
+
try {
|
|
9556
|
+
settings = JSON.parse(this.getFileContent());
|
|
9557
|
+
} catch (error) {
|
|
9558
|
+
throw new Error(`Failed to parse Reasonix hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
9559
|
+
}
|
|
9560
|
+
const hooks = reasonixHooksToCanonical(settings.hooks);
|
|
9561
|
+
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
|
|
9562
|
+
version: 1,
|
|
9563
|
+
hooks
|
|
9564
|
+
}, null, 2) });
|
|
9565
|
+
}
|
|
9566
|
+
validate() {
|
|
9567
|
+
return {
|
|
9568
|
+
success: true,
|
|
9569
|
+
error: null
|
|
9570
|
+
};
|
|
9571
|
+
}
|
|
9572
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
9573
|
+
return new ReasonixHooks({
|
|
9574
|
+
outputRoot,
|
|
9575
|
+
relativeDirPath,
|
|
9576
|
+
relativeFilePath,
|
|
9577
|
+
fileContent: JSON.stringify({ hooks: {} }, null, 2),
|
|
9578
|
+
validate: false
|
|
9579
|
+
});
|
|
9580
|
+
}
|
|
9581
|
+
};
|
|
9582
|
+
//#endregion
|
|
8498
9583
|
//#region src/features/hooks/vibe-hooks.ts
|
|
8499
9584
|
const VIBE_DIR = ".vibe";
|
|
8500
9585
|
const VIBE_HOOKS_FILE_NAME = "hooks.toml";
|
|
@@ -8653,6 +9738,12 @@ var VibeHooks = class VibeHooks extends ToolHooks {
|
|
|
8653
9738
|
relativeFilePath: VIBE_HOOKS_FILE_NAME
|
|
8654
9739
|
};
|
|
8655
9740
|
}
|
|
9741
|
+
static getExtraSharedWritePaths() {
|
|
9742
|
+
return [{
|
|
9743
|
+
relativeDirPath: VIBE_DIR,
|
|
9744
|
+
relativeFilePath: VIBE_CONFIG_FILE_NAME
|
|
9745
|
+
}];
|
|
9746
|
+
}
|
|
8656
9747
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
8657
9748
|
const paths = VibeHooks.getSettablePaths({ global });
|
|
8658
9749
|
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({});
|
|
@@ -8720,6 +9811,19 @@ var VibeHooks = class VibeHooks extends ToolHooks {
|
|
|
8720
9811
|
//#endregion
|
|
8721
9812
|
//#region src/features/hooks/hooks-processor.ts
|
|
8722
9813
|
const HooksProcessorToolTargetSchema = z.enum(hooksProcessorToolTargetTuple);
|
|
9814
|
+
/**
|
|
9815
|
+
* Event names present in the config that the target's adapter cannot emit.
|
|
9816
|
+
*
|
|
9817
|
+
* When the factory passes override-block keys through verbatim
|
|
9818
|
+
* (`passthroughOverrideEvents`), those keys are excluded from the check so
|
|
9819
|
+
* documented passthrough triggers aren't falsely reported as skipped.
|
|
9820
|
+
*/
|
|
9821
|
+
function unsupportedEventNames(params) {
|
|
9822
|
+
const { factory, sharedHooks, effectiveHooks } = params;
|
|
9823
|
+
const supportedEvents = new Set(factory.supportedEvents);
|
|
9824
|
+
const eventNames = factory.passthroughOverrideEvents ? Object.keys(sharedHooks) : Object.keys(effectiveHooks);
|
|
9825
|
+
return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
|
|
9826
|
+
}
|
|
8723
9827
|
const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
8724
9828
|
["antigravity-cli", {
|
|
8725
9829
|
class: AntigravityCliHooks,
|
|
@@ -8853,12 +9957,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
8853
9957
|
supportsGlobal: true,
|
|
8854
9958
|
supportsImport: true
|
|
8855
9959
|
},
|
|
8856
|
-
supportedEvents:
|
|
8857
|
-
supportedHookTypes: [
|
|
8858
|
-
"command",
|
|
8859
|
-
"prompt",
|
|
8860
|
-
"http"
|
|
8861
|
-
],
|
|
9960
|
+
supportedEvents: HERMESAGENT_HOOK_EVENTS,
|
|
9961
|
+
supportedHookTypes: ["command"],
|
|
8862
9962
|
supportsMatcher: true
|
|
8863
9963
|
}],
|
|
8864
9964
|
["deepagents", {
|
|
@@ -8894,6 +9994,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
8894
9994
|
supportedHookTypes: ["command"],
|
|
8895
9995
|
supportsMatcher: true
|
|
8896
9996
|
}],
|
|
9997
|
+
["kiro-ide", {
|
|
9998
|
+
class: KiroIdeHooks,
|
|
9999
|
+
meta: {
|
|
10000
|
+
supportsProject: true,
|
|
10001
|
+
supportsGlobal: true,
|
|
10002
|
+
supportsImport: true
|
|
10003
|
+
},
|
|
10004
|
+
supportedEvents: KIRO_IDE_HOOK_EVENTS,
|
|
10005
|
+
supportedHookTypes: ["command", "prompt"],
|
|
10006
|
+
supportsMatcher: true,
|
|
10007
|
+
passthroughOverrideEvents: true
|
|
10008
|
+
}],
|
|
8897
10009
|
["devin", {
|
|
8898
10010
|
class: DevinHooks,
|
|
8899
10011
|
meta: {
|
|
@@ -8902,8 +10014,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
8902
10014
|
supportsImport: true
|
|
8903
10015
|
},
|
|
8904
10016
|
supportedEvents: DEVIN_HOOK_EVENTS,
|
|
8905
|
-
supportedHookTypes: ["command"],
|
|
8906
|
-
supportsMatcher:
|
|
10017
|
+
supportedHookTypes: ["command", "prompt"],
|
|
10018
|
+
supportsMatcher: true
|
|
8907
10019
|
}],
|
|
8908
10020
|
["augmentcode", {
|
|
8909
10021
|
class: AugmentcodeHooks,
|
|
@@ -8948,6 +10060,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
8948
10060
|
supportedEvents: QWENCODE_HOOK_EVENTS,
|
|
8949
10061
|
supportedHookTypes: ["command"],
|
|
8950
10062
|
supportsMatcher: true
|
|
10063
|
+
}],
|
|
10064
|
+
["reasonix", {
|
|
10065
|
+
class: ReasonixHooks,
|
|
10066
|
+
meta: {
|
|
10067
|
+
supportsProject: true,
|
|
10068
|
+
supportsGlobal: true,
|
|
10069
|
+
supportsImport: true
|
|
10070
|
+
},
|
|
10071
|
+
supportedEvents: REASONIX_HOOK_EVENTS,
|
|
10072
|
+
supportedHookTypes: ["command"],
|
|
10073
|
+
supportsMatcher: true
|
|
8951
10074
|
}]
|
|
8952
10075
|
]);
|
|
8953
10076
|
const hooksProcessorToolTargets = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject).map(([t]) => t);
|
|
@@ -9023,8 +10146,11 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
9023
10146
|
...overrideHooks
|
|
9024
10147
|
};
|
|
9025
10148
|
{
|
|
9026
|
-
const
|
|
9027
|
-
|
|
10149
|
+
const skipped = unsupportedEventNames({
|
|
10150
|
+
factory,
|
|
10151
|
+
sharedHooks,
|
|
10152
|
+
effectiveHooks
|
|
10153
|
+
});
|
|
9028
10154
|
if (skipped.length > 0) this.logger.warn(`Skipped hook event(s) for ${this.toolTarget} (not supported): ${skipped.join(", ")}`);
|
|
9029
10155
|
}
|
|
9030
10156
|
{
|
|
@@ -9220,11 +10346,6 @@ var AiassistantIgnore = class AiassistantIgnore extends ToolIgnore {
|
|
|
9220
10346
|
}
|
|
9221
10347
|
};
|
|
9222
10348
|
//#endregion
|
|
9223
|
-
//#region src/constants/antigravity-cli-paths.ts
|
|
9224
|
-
const ANTIGRAVITY_AGENTS_DIR = ANTIGRAVITY_DIR;
|
|
9225
|
-
const ANTIGRAVITY_RULE_FILE_NAME = "AGENTS.md";
|
|
9226
|
-
const ANTIGRAVITY_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
|
|
9227
|
-
//#endregion
|
|
9228
10349
|
//#region src/features/ignore/antigravity-cli-ignore.ts
|
|
9229
10350
|
/**
|
|
9230
10351
|
* Antigravity CLI is the successor to Gemini CLI and is built on the same
|
|
@@ -10456,9 +11577,9 @@ function parseAmpSettingsJsonc(fileContent) {
|
|
|
10456
11577
|
}
|
|
10457
11578
|
function filterMcpServers(mcpServers) {
|
|
10458
11579
|
const filtered = {};
|
|
10459
|
-
if (!isRecord
|
|
11580
|
+
if (!isRecord(mcpServers)) return filtered;
|
|
10460
11581
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
10461
|
-
if (isPrototypePollutionKey(name) || !isRecord
|
|
11582
|
+
if (isPrototypePollutionKey(name) || !isRecord(config)) continue;
|
|
10462
11583
|
const filteredConfig = {};
|
|
10463
11584
|
for (const [key, value] of Object.entries(config)) {
|
|
10464
11585
|
if (isPrototypePollutionKey(key)) continue;
|
|
@@ -10573,7 +11694,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
10573
11694
|
success: true,
|
|
10574
11695
|
error: null
|
|
10575
11696
|
};
|
|
10576
|
-
if (!isRecord
|
|
11697
|
+
if (!isRecord(mcpServers)) return {
|
|
10577
11698
|
success: false,
|
|
10578
11699
|
error: /* @__PURE__ */ new Error(`${AMP_MCP_SERVERS_KEY} must be a JSON object`)
|
|
10579
11700
|
};
|
|
@@ -10582,7 +11703,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
10582
11703
|
success: false,
|
|
10583
11704
|
error: /* @__PURE__ */ new Error(`Server name "${serverName}" is a prototype pollution key and is not allowed`)
|
|
10584
11705
|
};
|
|
10585
|
-
if (!isRecord
|
|
11706
|
+
if (!isRecord(serverConfig)) return {
|
|
10586
11707
|
success: false,
|
|
10587
11708
|
error: /* @__PURE__ */ new Error(`MCP server "${serverName}" must be a JSON object`)
|
|
10588
11709
|
};
|
|
@@ -11103,7 +12224,7 @@ const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
|
|
|
11103
12224
|
function convertFromCodexFormat(codexMcp) {
|
|
11104
12225
|
const result = {};
|
|
11105
12226
|
for (const [name, config] of Object.entries(codexMcp)) {
|
|
11106
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
12227
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
11107
12228
|
const converted = {};
|
|
11108
12229
|
for (const [key, value] of Object.entries(config)) {
|
|
11109
12230
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
@@ -11123,7 +12244,7 @@ function convertToCodexFormat(mcpServers) {
|
|
|
11123
12244
|
const result = {};
|
|
11124
12245
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
11125
12246
|
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
11126
|
-
if (!isRecord
|
|
12247
|
+
if (!isRecord(config)) continue;
|
|
11127
12248
|
const converted = {};
|
|
11128
12249
|
for (const [key, value] of Object.entries(config)) {
|
|
11129
12250
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
@@ -11185,19 +12306,19 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
|
|
|
11185
12306
|
const strippedMcpServers = rulesyncMcp.getMcpServers();
|
|
11186
12307
|
const rawMcpServers = rulesyncMcp.getJson().mcpServers;
|
|
11187
12308
|
const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
|
|
11188
|
-
const rawServer = isRecord
|
|
12309
|
+
const rawServer = isRecord(rawMcpServers) ? rawMcpServers[serverName] : void 0;
|
|
11189
12310
|
return [serverName, {
|
|
11190
12311
|
...serverConfig,
|
|
11191
|
-
...isRecord
|
|
12312
|
+
...isRecord(rawServer) && isStringArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {}
|
|
11192
12313
|
}];
|
|
11193
12314
|
})));
|
|
11194
12315
|
const filteredMcpServers = this.removeEmptyEntries(converted);
|
|
11195
12316
|
for (const name of Object.keys(converted)) if (!Object.hasOwn(filteredMcpServers, name)) warnWithFallback(void 0, `MCP server "${name}" had no non-empty configuration and was dropped from the codex CLI config`);
|
|
11196
|
-
const existingMcpServers = isRecord
|
|
12317
|
+
const existingMcpServers = isRecord(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
|
|
11197
12318
|
configToml["mcp_servers"] = Object.fromEntries(Object.entries(filteredMcpServers).map(([name, serverConfig]) => {
|
|
11198
|
-
const existingServer = isRecord
|
|
12319
|
+
const existingServer = isRecord(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
|
|
11199
12320
|
const serverRecord = serverConfig;
|
|
11200
|
-
if (existingServer && isRecord
|
|
12321
|
+
if (existingServer && isRecord(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
|
|
11201
12322
|
...serverRecord,
|
|
11202
12323
|
tools: existingServer["tools"]
|
|
11203
12324
|
}];
|
|
@@ -11707,18 +12828,20 @@ var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
|
|
|
11707
12828
|
//#endregion
|
|
11708
12829
|
//#region src/features/mcp/devin-mcp.ts
|
|
11709
12830
|
/**
|
|
11710
|
-
* MCP generator for Devin (
|
|
12831
|
+
* MCP generator for Devin Local (native `.devin/` configuration).
|
|
12832
|
+
*
|
|
12833
|
+
* Devin reads MCP servers from the `mcpServers` key of its native config file:
|
|
12834
|
+
* - Project scope: `.devin/config.json`
|
|
12835
|
+
* - Global scope: `~/.config/devin/config.json`
|
|
11711
12836
|
*
|
|
11712
|
-
*
|
|
11713
|
-
*
|
|
11714
|
-
*
|
|
12837
|
+
* The config file is shared with the permissions feature (`permissions` key)
|
|
12838
|
+
* and, in global mode, the hooks feature (`hooks` key), so reads and writes
|
|
12839
|
+
* merge into the existing JSON rather than overwriting it, and the file is
|
|
12840
|
+
* never deleted. Each server is a stdio entry ({ command, args, env }) or a
|
|
12841
|
+
* remote entry ({ serverUrl | url, headers }), and may carry an optional
|
|
12842
|
+
* `disabledTools` array.
|
|
11715
12843
|
*
|
|
11716
|
-
*
|
|
11717
|
-
* the same `mcp_config.json` filename so both scopes fit the processor
|
|
11718
|
-
* framework cleanly. The top-level key is `mcpServers`; each server is a
|
|
11719
|
-
* stdio entry ({ command, args, env }) or a remote entry
|
|
11720
|
-
* ({ serverUrl | url, headers }), and may carry an optional `disabledTools`
|
|
11721
|
-
* array.
|
|
12844
|
+
* @see https://docs.devin.ai/cli/extensibility/configuration
|
|
11722
12845
|
*/
|
|
11723
12846
|
var DevinMcp = class DevinMcp extends ToolMcp {
|
|
11724
12847
|
json;
|
|
@@ -11736,14 +12859,21 @@ var DevinMcp = class DevinMcp extends ToolMcp {
|
|
|
11736
12859
|
throw new Error(`Failed to parse Devin MCP config at ${join(relativeDirPath, relativeFilePath)}: ${formatError(error)}`, { cause: error });
|
|
11737
12860
|
}
|
|
11738
12861
|
}
|
|
12862
|
+
/**
|
|
12863
|
+
* config.json may carry the permissions/hooks features' keys, so it is never
|
|
12864
|
+
* deleted; only the managed `mcpServers` key is rewritten.
|
|
12865
|
+
*/
|
|
12866
|
+
isDeletable() {
|
|
12867
|
+
return false;
|
|
12868
|
+
}
|
|
11739
12869
|
static getSettablePaths({ global = false } = {}) {
|
|
11740
12870
|
if (global) return {
|
|
11741
|
-
relativeDirPath:
|
|
11742
|
-
relativeFilePath:
|
|
12871
|
+
relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
|
|
12872
|
+
relativeFilePath: DEVIN_CONFIG_FILE_NAME
|
|
11743
12873
|
};
|
|
11744
12874
|
return {
|
|
11745
|
-
relativeDirPath:
|
|
11746
|
-
relativeFilePath:
|
|
12875
|
+
relativeDirPath: DEVIN_DIR,
|
|
12876
|
+
relativeFilePath: DEVIN_CONFIG_FILE_NAME
|
|
11747
12877
|
};
|
|
11748
12878
|
}
|
|
11749
12879
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
@@ -11858,7 +12988,7 @@ var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
|
|
|
11858
12988
|
};
|
|
11859
12989
|
//#endregion
|
|
11860
12990
|
//#region src/features/mcp/goose-mcp.ts
|
|
11861
|
-
const
|
|
12991
|
+
const GOOSE_PLUGIN_MCP_RELATIVE_PATH = join(GOOSE_PLUGIN_MCP_DIR_PATH, GOOSE_PLUGIN_MCP_FILE_NAME);
|
|
11862
12992
|
function parseGooseConfig(fileContent, relativeDirPath, relativeFilePath) {
|
|
11863
12993
|
const configPath = join(relativeDirPath, relativeFilePath);
|
|
11864
12994
|
let parsed;
|
|
@@ -11945,7 +13075,7 @@ function convertServerToGooseExtension(name, config) {
|
|
|
11945
13075
|
function convertToGooseFormat(mcpServers) {
|
|
11946
13076
|
const extensions = {};
|
|
11947
13077
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
11948
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
13078
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
11949
13079
|
extensions[name] = convertServerToGooseExtension(name, config);
|
|
11950
13080
|
}
|
|
11951
13081
|
return extensions;
|
|
@@ -11962,7 +13092,7 @@ function convertToGooseFormat(mcpServers) {
|
|
|
11962
13092
|
function convertFromGooseFormat(extensions) {
|
|
11963
13093
|
const result = {};
|
|
11964
13094
|
for (const [name, ext] of Object.entries(extensions)) {
|
|
11965
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
13095
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(ext)) continue;
|
|
11966
13096
|
const server = {};
|
|
11967
13097
|
const type = typeof ext.type === "string" ? ext.type : void 0;
|
|
11968
13098
|
if (type === "sse") server.type = "sse";
|
|
@@ -11980,39 +13110,102 @@ function convertFromGooseFormat(extensions) {
|
|
|
11980
13110
|
return result;
|
|
11981
13111
|
}
|
|
11982
13112
|
/**
|
|
13113
|
+
* Builds a Claude-style stdio server entry (`command`/`args`/`env`/`cwd`) for the
|
|
13114
|
+
* open-plugins `.mcp.json` manifest.
|
|
13115
|
+
*/
|
|
13116
|
+
function buildGoosePluginStdioServer(config) {
|
|
13117
|
+
const server = {};
|
|
13118
|
+
const command = config.command;
|
|
13119
|
+
if (Array.isArray(command)) {
|
|
13120
|
+
if (typeof command[0] === "string") server.command = command[0];
|
|
13121
|
+
const rest = command.slice(1).filter((c) => typeof c === "string");
|
|
13122
|
+
const args = isStringArray(config.args) ? config.args : [];
|
|
13123
|
+
if (rest.length > 0 || args.length > 0) server.args = [...rest, ...args];
|
|
13124
|
+
} else if (typeof command === "string") {
|
|
13125
|
+
server.command = command;
|
|
13126
|
+
if (isStringArray(config.args)) server.args = config.args;
|
|
13127
|
+
}
|
|
13128
|
+
if (isPlainObject(config.env)) server.env = omitPrototypePollutionKeys(config.env);
|
|
13129
|
+
if (typeof config.cwd === "string") server.cwd = config.cwd;
|
|
13130
|
+
return server;
|
|
13131
|
+
}
|
|
13132
|
+
/**
|
|
13133
|
+
* Converts rulesync canonical MCP servers into the Claude-style `mcpServers` map
|
|
13134
|
+
* used by Goose open-plugin manifests (`.agents/plugins/rulesync/.mcp.json`).
|
|
13135
|
+
*
|
|
13136
|
+
* The open-plugins manifest is **stdio-only** (no `url`/`headers`), so remote
|
|
13137
|
+
* (http/sse/streamable_http) and `builtin` servers cannot be represented. They
|
|
13138
|
+
* are skipped with a warning rather than silently dropped — remote servers stay
|
|
13139
|
+
* global-config-only (sync them with `--global` to `~/.config/goose/config.yaml`).
|
|
13140
|
+
*/
|
|
13141
|
+
function convertToGoosePluginMcpServers(mcpServers, logger) {
|
|
13142
|
+
const result = {};
|
|
13143
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
13144
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
13145
|
+
const gooseType = resolveGooseType(config, resolveGooseUrl(config));
|
|
13146
|
+
if (gooseType !== "stdio") {
|
|
13147
|
+
warnWithFallback(logger, `Goose open-plugin MCP manifest (${GOOSE_PLUGIN_MCP_RELATIVE_PATH}) is stdio-only; skipping "${name}" (${gooseType}). Sync it with --global to ~/.config/goose/config.yaml instead.`);
|
|
13148
|
+
continue;
|
|
13149
|
+
}
|
|
13150
|
+
result[name] = buildGoosePluginStdioServer(config);
|
|
13151
|
+
}
|
|
13152
|
+
return result;
|
|
13153
|
+
}
|
|
13154
|
+
/**
|
|
11983
13155
|
* Goose MCP servers.
|
|
11984
13156
|
*
|
|
11985
|
-
* Goose configures MCP servers
|
|
11986
|
-
*
|
|
11987
|
-
*
|
|
11988
|
-
*
|
|
11989
|
-
*
|
|
13157
|
+
* Goose configures MCP servers in two locations:
|
|
13158
|
+
*
|
|
13159
|
+
* - **Global** (`--global`): "extensions" in the shared user config
|
|
13160
|
+
* `~/.config/goose/config.yaml`. That file also holds other Goose settings
|
|
13161
|
+
* (model, provider, ...), so generation merges the `extensions:` block into the
|
|
13162
|
+
* existing config instead of overwriting it, and the file is never deleted.
|
|
13163
|
+
* This location supports both stdio and remote (http/sse) servers.
|
|
13164
|
+
* - **Project**: a stdio-only open-plugin manifest at
|
|
13165
|
+
* `.agents/plugins/rulesync/.mcp.json` (Goose v1.39.0+), reusing the same
|
|
13166
|
+
* `.agents/plugins/rulesync/` tree as Goose hooks. The manifest uses the
|
|
13167
|
+
* Claude-style `{ "mcpServers": { "<name>": { command, args, env, cwd } } }`
|
|
13168
|
+
* shape and cannot express `url`/`headers`, so remote servers are skipped with
|
|
13169
|
+
* a warning in project mode (use `--global` to sync them instead).
|
|
11990
13170
|
*
|
|
11991
13171
|
* @see https://block.github.io/goose/docs/getting-started/using-extensions/
|
|
13172
|
+
* @see https://github.com/block/goose/pull/9471
|
|
11992
13173
|
*/
|
|
11993
13174
|
var GooseMcp = class GooseMcp extends ToolMcp {
|
|
11994
13175
|
config;
|
|
11995
13176
|
constructor(params) {
|
|
11996
13177
|
super(params);
|
|
11997
|
-
if (this.fileContent
|
|
11998
|
-
else this.config =
|
|
13178
|
+
if (this.fileContent === void 0) this.config = {};
|
|
13179
|
+
else if (params.global) this.config = parseGooseConfig(this.fileContent, this.relativeDirPath, this.relativeFilePath);
|
|
13180
|
+
else this.config = this.fileContent ? this.parsePluginManifest(this.fileContent) : {};
|
|
13181
|
+
}
|
|
13182
|
+
parsePluginManifest(fileContent) {
|
|
13183
|
+
try {
|
|
13184
|
+
const parsed = JSON.parse(fileContent);
|
|
13185
|
+
return isRecord(parsed) ? parsed : {};
|
|
13186
|
+
} catch (error) {
|
|
13187
|
+
throw new Error(`Failed to parse Goose MCP manifest at ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(error)}`, { cause: error });
|
|
13188
|
+
}
|
|
11999
13189
|
}
|
|
12000
13190
|
getConfig() {
|
|
12001
13191
|
return this.config;
|
|
12002
13192
|
}
|
|
12003
13193
|
isDeletable() {
|
|
12004
|
-
return
|
|
13194
|
+
return !this.global;
|
|
12005
13195
|
}
|
|
12006
|
-
static getSettablePaths(
|
|
12007
|
-
return {
|
|
13196
|
+
static getSettablePaths({ global = false } = {}) {
|
|
13197
|
+
if (global) return {
|
|
12008
13198
|
relativeDirPath: GOOSE_GLOBAL_DIR,
|
|
12009
13199
|
relativeFilePath: GOOSE_MCP_FILE_NAME
|
|
12010
13200
|
};
|
|
13201
|
+
return {
|
|
13202
|
+
relativeDirPath: GOOSE_PLUGIN_MCP_DIR_PATH,
|
|
13203
|
+
relativeFilePath: GOOSE_PLUGIN_MCP_FILE_NAME
|
|
13204
|
+
};
|
|
12011
13205
|
}
|
|
12012
13206
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
12013
|
-
if (!global) throw new Error(GOOSE_GLOBAL_ONLY_MESSAGE$1);
|
|
12014
13207
|
const paths = this.getSettablePaths({ global });
|
|
12015
|
-
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
13208
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? (global ? "" : JSON.stringify({ mcpServers: {} }, null, 2));
|
|
12016
13209
|
return new GooseMcp({
|
|
12017
13210
|
outputRoot,
|
|
12018
13211
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -12022,9 +13215,19 @@ var GooseMcp = class GooseMcp extends ToolMcp {
|
|
|
12022
13215
|
global
|
|
12023
13216
|
});
|
|
12024
13217
|
}
|
|
12025
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
12026
|
-
if (!global) throw new Error(GOOSE_GLOBAL_ONLY_MESSAGE$1);
|
|
13218
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
12027
13219
|
const paths = this.getSettablePaths({ global });
|
|
13220
|
+
if (!global) {
|
|
13221
|
+
const mcpServers = convertToGoosePluginMcpServers(rulesyncMcp.getMcpServers(), logger);
|
|
13222
|
+
return new GooseMcp({
|
|
13223
|
+
outputRoot,
|
|
13224
|
+
relativeDirPath: paths.relativeDirPath,
|
|
13225
|
+
relativeFilePath: paths.relativeFilePath,
|
|
13226
|
+
fileContent: JSON.stringify({ mcpServers }, null, 2),
|
|
13227
|
+
validate,
|
|
13228
|
+
global
|
|
13229
|
+
});
|
|
13230
|
+
}
|
|
12028
13231
|
const merged = {
|
|
12029
13232
|
...parseGooseConfig(await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), ""), paths.relativeDirPath, paths.relativeFilePath),
|
|
12030
13233
|
extensions: convertToGooseFormat(rulesyncMcp.getMcpServers())
|
|
@@ -12039,7 +13242,11 @@ var GooseMcp = class GooseMcp extends ToolMcp {
|
|
|
12039
13242
|
});
|
|
12040
13243
|
}
|
|
12041
13244
|
toRulesyncMcp() {
|
|
12042
|
-
|
|
13245
|
+
if (!this.global) {
|
|
13246
|
+
const mcpServers = isRecord(this.config.mcpServers) ? this.config.mcpServers : {};
|
|
13247
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
13248
|
+
}
|
|
13249
|
+
const mcpServers = convertFromGooseFormat(isRecord(this.config.extensions) ? this.config.extensions : {});
|
|
12043
13250
|
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
12044
13251
|
}
|
|
12045
13252
|
validate() {
|
|
@@ -12116,7 +13323,7 @@ function convertToGrokFormat(mcpServers) {
|
|
|
12116
13323
|
const result = {};
|
|
12117
13324
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
12118
13325
|
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
12119
|
-
if (!isRecord
|
|
13326
|
+
if (!isRecord(config)) continue;
|
|
12120
13327
|
const converted = {};
|
|
12121
13328
|
for (const [key, value] of Object.entries(config)) {
|
|
12122
13329
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
@@ -12131,7 +13338,7 @@ function convertToGrokFormat(mcpServers) {
|
|
|
12131
13338
|
function convertFromGrokFormat(grokMcp) {
|
|
12132
13339
|
const result = {};
|
|
12133
13340
|
for (const [name, config] of Object.entries(grokMcp)) {
|
|
12134
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
13341
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
12135
13342
|
const converted = {};
|
|
12136
13343
|
for (const [key, value] of Object.entries(config)) {
|
|
12137
13344
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
@@ -12294,13 +13501,13 @@ function convertServerToHermes(config) {
|
|
|
12294
13501
|
function convertToHermesFormat(mcpServers) {
|
|
12295
13502
|
const result = {};
|
|
12296
13503
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
12297
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
13504
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
12298
13505
|
result[name] = convertServerToHermes(config);
|
|
12299
13506
|
}
|
|
12300
13507
|
return result;
|
|
12301
13508
|
}
|
|
12302
13509
|
function mergeHermesMcpServers(config, mcpServers) {
|
|
12303
|
-
const existingMcpServers = isRecord
|
|
13510
|
+
const existingMcpServers = isRecord(config.mcp_servers) ? config.mcp_servers : {};
|
|
12304
13511
|
return {
|
|
12305
13512
|
...config,
|
|
12306
13513
|
mcp_servers: {
|
|
@@ -12318,7 +13525,7 @@ function mergeHermesMcpServers(config, mcpServers) {
|
|
|
12318
13525
|
function convertFromHermesFormat(mcpServers) {
|
|
12319
13526
|
const result = {};
|
|
12320
13527
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
12321
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
13528
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
12322
13529
|
const server = {};
|
|
12323
13530
|
if (typeof config.command === "string") server.command = config.command;
|
|
12324
13531
|
if (isStringArray(config.args)) server.args = config.args;
|
|
@@ -12354,7 +13561,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
12354
13561
|
return true;
|
|
12355
13562
|
}
|
|
12356
13563
|
setFileContent(fileContent) {
|
|
12357
|
-
const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord
|
|
13564
|
+
const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
12358
13565
|
this.config = merged;
|
|
12359
13566
|
super.setFileContent(stringifyHermesConfig(merged));
|
|
12360
13567
|
}
|
|
@@ -12394,7 +13601,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
12394
13601
|
});
|
|
12395
13602
|
}
|
|
12396
13603
|
toRulesyncMcp() {
|
|
12397
|
-
const servers = convertFromHermesFormat(isRecord
|
|
13604
|
+
const servers = convertFromHermesFormat(isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
12398
13605
|
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: servers }, null, 2) });
|
|
12399
13606
|
}
|
|
12400
13607
|
validate() {
|
|
@@ -13229,11 +14436,6 @@ var QwencodeMcp = class QwencodeMcp extends ToolMcp {
|
|
|
13229
14436
|
}
|
|
13230
14437
|
};
|
|
13231
14438
|
//#endregion
|
|
13232
|
-
//#region src/constants/reasonix-paths.ts
|
|
13233
|
-
const REASONIX_PROJECT_MCP_FILE_NAME = "reasonix.toml";
|
|
13234
|
-
const REASONIX_GLOBAL_DIR = ".reasonix";
|
|
13235
|
-
const REASONIX_GLOBAL_MCP_FILE_NAME = "config.toml";
|
|
13236
|
-
//#endregion
|
|
13237
14439
|
//#region src/features/mcp/reasonix-mcp.ts
|
|
13238
14440
|
const REASONIX_PLUGIN_FIELDS = [
|
|
13239
14441
|
"type",
|
|
@@ -13241,13 +14443,14 @@ const REASONIX_PLUGIN_FIELDS = [
|
|
|
13241
14443
|
"args",
|
|
13242
14444
|
"env",
|
|
13243
14445
|
"url",
|
|
13244
|
-
"headers"
|
|
14446
|
+
"headers",
|
|
14447
|
+
"trusted_read_only_tools"
|
|
13245
14448
|
];
|
|
13246
14449
|
var ReasonixMcp = class ReasonixMcp extends ToolMcp {
|
|
13247
14450
|
toml;
|
|
13248
14451
|
constructor(params) {
|
|
13249
14452
|
super(params);
|
|
13250
|
-
this.toml = parseReasonixConfig(this.fileContent);
|
|
14453
|
+
this.toml = parseReasonixConfig$1(this.fileContent);
|
|
13251
14454
|
}
|
|
13252
14455
|
getToml() {
|
|
13253
14456
|
return this.toml;
|
|
@@ -13271,7 +14474,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
|
|
|
13271
14474
|
}
|
|
13272
14475
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
13273
14476
|
const paths = this.getSettablePaths({ global });
|
|
13274
|
-
const config = parseReasonixConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
|
|
14477
|
+
const config = parseReasonixConfig$1(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
|
|
13275
14478
|
config.plugins = normalizePluginsArray(config.plugins);
|
|
13276
14479
|
return new ReasonixMcp({
|
|
13277
14480
|
outputRoot,
|
|
@@ -13284,7 +14487,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
|
|
|
13284
14487
|
}
|
|
13285
14488
|
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
13286
14489
|
const paths = this.getSettablePaths({ global });
|
|
13287
|
-
const config = parseReasonixConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
|
|
14490
|
+
const config = parseReasonixConfig$1(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
|
|
13288
14491
|
config.plugins = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToReasonix(name, server));
|
|
13289
14492
|
return new ReasonixMcp({
|
|
13290
14493
|
outputRoot,
|
|
@@ -13301,7 +14504,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
|
|
|
13301
14504
|
}
|
|
13302
14505
|
validate() {
|
|
13303
14506
|
try {
|
|
13304
|
-
parseReasonixConfig(this.fileContent);
|
|
14507
|
+
parseReasonixConfig$1(this.fileContent);
|
|
13305
14508
|
return {
|
|
13306
14509
|
success: true,
|
|
13307
14510
|
error: null
|
|
@@ -13324,7 +14527,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
|
|
|
13324
14527
|
});
|
|
13325
14528
|
}
|
|
13326
14529
|
};
|
|
13327
|
-
function parseReasonixConfig(fileContent) {
|
|
14530
|
+
function parseReasonixConfig$1(fileContent) {
|
|
13328
14531
|
const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
|
|
13329
14532
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
13330
14533
|
return { ...parsed };
|
|
@@ -13464,17 +14667,6 @@ var RooMcp = class RooMcp extends ToolMcp {
|
|
|
13464
14667
|
}
|
|
13465
14668
|
};
|
|
13466
14669
|
//#endregion
|
|
13467
|
-
//#region src/constants/rovodev-paths.ts
|
|
13468
|
-
const ROVODEV_DIR = ".rovodev";
|
|
13469
|
-
const ROVODEV_SKILLS_DIR_PATH = join(ROVODEV_DIR, "skills");
|
|
13470
|
-
const ROVODEV_SUBAGENTS_DIR_PATH = join(ROVODEV_DIR, "subagents");
|
|
13471
|
-
const ROVODEV_MODULAR_RULES_DIR_PATH = join(ROVODEV_DIR, ".rulesync", "modular-rules");
|
|
13472
|
-
const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
|
|
13473
|
-
const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
|
|
13474
|
-
const ROVODEV_MCP_FILE_NAME = "mcp.json";
|
|
13475
|
-
const ROVODEV_CONFIG_FILE_NAME = "config.yml";
|
|
13476
|
-
const ROVODEV_AGENTS_SKILLS_DIR_PATH = join(".agents", "skills");
|
|
13477
|
-
//#endregion
|
|
13478
14670
|
//#region src/features/mcp/rovodev-mcp.ts
|
|
13479
14671
|
function parseRovodevMcpJson(fileContent, relativeDirPath, relativeFilePath) {
|
|
13480
14672
|
const configPath = join(relativeDirPath, relativeFilePath);
|
|
@@ -13568,6 +14760,178 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
13568
14760
|
}
|
|
13569
14761
|
};
|
|
13570
14762
|
//#endregion
|
|
14763
|
+
//#region src/features/shared/takt-config.ts
|
|
14764
|
+
/**
|
|
14765
|
+
* Parse a Takt `config.yaml` into a plain object, treating an empty file as `{}`.
|
|
14766
|
+
*
|
|
14767
|
+
* Shared by the Takt adapters that read-modify-write the same `config.yaml`
|
|
14768
|
+
* (mcp, permissions, ...). Uses `isPlainObject` (not `isRecord`) so class
|
|
14769
|
+
* instances are rejected for prototype-pollution hardening; a YAML mapping
|
|
14770
|
+
* always parses to a plain object.
|
|
14771
|
+
*/
|
|
14772
|
+
function parseTaktConfig(fileContent, relativeDirPath, relativeFilePath) {
|
|
14773
|
+
const configPath = join(relativeDirPath, relativeFilePath);
|
|
14774
|
+
let parsed;
|
|
14775
|
+
try {
|
|
14776
|
+
parsed = fileContent.trim() === "" ? {} : load(fileContent);
|
|
14777
|
+
} catch (error) {
|
|
14778
|
+
throw new Error(`Failed to parse Takt config at ${configPath}: ${formatError(error)}`, { cause: error });
|
|
14779
|
+
}
|
|
14780
|
+
if (parsed === void 0 || parsed === null) return {};
|
|
14781
|
+
if (!isPlainObject(parsed)) throw new Error(`Failed to parse Takt config at ${configPath}: expected a YAML mapping`);
|
|
14782
|
+
return parsed;
|
|
14783
|
+
}
|
|
14784
|
+
//#endregion
|
|
14785
|
+
//#region src/features/mcp/takt-mcp.ts
|
|
14786
|
+
/**
|
|
14787
|
+
* MCP adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
|
|
14788
|
+
* global).
|
|
14789
|
+
*
|
|
14790
|
+
* IMPORTANT — what this adapter can and cannot represent.
|
|
14791
|
+
*
|
|
14792
|
+
* Takt does NOT have a project- or global-level registry of MCP *server
|
|
14793
|
+
* definitions*. The concrete `mcp_servers` map (a server's `command`/`args`/`env`
|
|
14794
|
+
* or `type`/`url`/`headers`) is declared per-step inside individual *workflow*
|
|
14795
|
+
* YAML files; there is no top-level `mcp_servers` key in `config.yaml`, and the
|
|
14796
|
+
* config loader hard-rejects unknown top-level keys
|
|
14797
|
+
* (`assertNoUnknownGlobalConfigKeys`). Writing a server map into `config.yaml`
|
|
14798
|
+
* would therefore both be ignored and break the user's config.
|
|
14799
|
+
*
|
|
14800
|
+
* What `config.yaml` *does* hold is the default-deny transport allowlist
|
|
14801
|
+
* `workflow_mcp_servers: { stdio, sse, http }`. Without it, workflow-defined MCP
|
|
14802
|
+
* servers are refused regardless of how they are declared. So this adapter emits
|
|
14803
|
+
* the transport allowlist derived from the transports present in
|
|
14804
|
+
* `.rulesync/mcp.json`, enabling exactly the transports the user's servers need.
|
|
14805
|
+
*
|
|
14806
|
+
* Lossiness (documented, intentional): the per-server names, commands, env, URLs
|
|
14807
|
+
* and headers are NOT representable in `config.yaml` and are intentionally not
|
|
14808
|
+
* written. Users still declare the concrete servers in their workflow YAML
|
|
14809
|
+
* steps; rulesync only opens the transport gate that permits them. As a
|
|
14810
|
+
* corollary, import (`toRulesyncMcp`) cannot reconstruct server definitions from
|
|
14811
|
+
* a transport allowlist and yields an empty `mcpServers` map.
|
|
14812
|
+
*
|
|
14813
|
+
* The shared `config.yaml` is merged in place: only the
|
|
14814
|
+
* `workflow_mcp_servers` key is set; every other top-level key (provider,
|
|
14815
|
+
* provider_profiles, etc.) is preserved. The file is never deleted.
|
|
14816
|
+
*
|
|
14817
|
+
* @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
|
|
14818
|
+
* @see https://github.com/nrslib/takt/blob/main/src/core/models/mcp-schemas.ts
|
|
14819
|
+
*/
|
|
14820
|
+
var TaktMcp = class TaktMcp extends ToolMcp {
|
|
14821
|
+
constructor(params) {
|
|
14822
|
+
super({
|
|
14823
|
+
...params,
|
|
14824
|
+
fileContent: params.fileContent ?? ""
|
|
14825
|
+
});
|
|
14826
|
+
}
|
|
14827
|
+
isDeletable() {
|
|
14828
|
+
return false;
|
|
14829
|
+
}
|
|
14830
|
+
static getSettablePaths(_options) {
|
|
14831
|
+
return {
|
|
14832
|
+
relativeDirPath: TAKT_DIR,
|
|
14833
|
+
relativeFilePath: TAKT_CONFIG_FILE_NAME
|
|
14834
|
+
};
|
|
14835
|
+
}
|
|
14836
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
14837
|
+
const paths = TaktMcp.getSettablePaths({ global });
|
|
14838
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
14839
|
+
return new TaktMcp({
|
|
14840
|
+
outputRoot,
|
|
14841
|
+
relativeDirPath: paths.relativeDirPath,
|
|
14842
|
+
relativeFilePath: paths.relativeFilePath,
|
|
14843
|
+
fileContent,
|
|
14844
|
+
validate,
|
|
14845
|
+
global
|
|
14846
|
+
});
|
|
14847
|
+
}
|
|
14848
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
14849
|
+
const paths = TaktMcp.getSettablePaths({ global });
|
|
14850
|
+
const config = parseTaktConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
|
|
14851
|
+
const allowlist = deriveTransportAllowlist(rulesyncMcp.getMcpServers());
|
|
14852
|
+
const merged = {
|
|
14853
|
+
...config,
|
|
14854
|
+
[TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist
|
|
14855
|
+
};
|
|
14856
|
+
return new TaktMcp({
|
|
14857
|
+
outputRoot,
|
|
14858
|
+
relativeDirPath: paths.relativeDirPath,
|
|
14859
|
+
relativeFilePath: paths.relativeFilePath,
|
|
14860
|
+
fileContent: dump(merged),
|
|
14861
|
+
validate,
|
|
14862
|
+
global
|
|
14863
|
+
});
|
|
14864
|
+
}
|
|
14865
|
+
/**
|
|
14866
|
+
* A transport allowlist cannot reconstruct the per-step server definitions,
|
|
14867
|
+
* so import yields an empty `mcpServers` map. This keeps the round-trip honest
|
|
14868
|
+
* rather than fabricating placeholder servers.
|
|
14869
|
+
*/
|
|
14870
|
+
toRulesyncMcp() {
|
|
14871
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: {} }, null, 2) });
|
|
14872
|
+
}
|
|
14873
|
+
validate() {
|
|
14874
|
+
return {
|
|
14875
|
+
success: true,
|
|
14876
|
+
error: null
|
|
14877
|
+
};
|
|
14878
|
+
}
|
|
14879
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
14880
|
+
return new TaktMcp({
|
|
14881
|
+
outputRoot,
|
|
14882
|
+
relativeDirPath,
|
|
14883
|
+
relativeFilePath,
|
|
14884
|
+
fileContent: "",
|
|
14885
|
+
validate: false,
|
|
14886
|
+
global
|
|
14887
|
+
});
|
|
14888
|
+
}
|
|
14889
|
+
};
|
|
14890
|
+
/**
|
|
14891
|
+
* Map a rulesync MCP server onto the single Takt transport its allowlist gates.
|
|
14892
|
+
*
|
|
14893
|
+
* Takt allows only `stdio` / `sse` / `http`, so the broader rulesync alias set is
|
|
14894
|
+
* folded: `local` ⇒ stdio; `streamable-http` / `ws` ⇒ http. A server with no
|
|
14895
|
+
* explicit transport is treated as stdio when it carries a `command`, else as a
|
|
14896
|
+
* remote `http` server (it must have a `url`). Returns `undefined` only when the
|
|
14897
|
+
* shape is too ambiguous to classify.
|
|
14898
|
+
*/
|
|
14899
|
+
function transportOf(server) {
|
|
14900
|
+
switch (server.type ?? server.transport) {
|
|
14901
|
+
case "stdio":
|
|
14902
|
+
case "local": return "stdio";
|
|
14903
|
+
case "sse": return "sse";
|
|
14904
|
+
case "http":
|
|
14905
|
+
case "streamable-http":
|
|
14906
|
+
case "ws": return "http";
|
|
14907
|
+
default: break;
|
|
14908
|
+
}
|
|
14909
|
+
if (server.command !== void 0) return "stdio";
|
|
14910
|
+
if (server.url !== void 0 || server.httpUrl !== void 0) return "http";
|
|
14911
|
+
}
|
|
14912
|
+
/**
|
|
14913
|
+
* Derive Takt's `workflow_mcp_servers` allowlist from the transports present in
|
|
14914
|
+
* the rulesync servers. All three keys are emitted explicitly (default-deny made
|
|
14915
|
+
* visible): a transport is `true` only when at least one server uses it.
|
|
14916
|
+
*
|
|
14917
|
+
* Server entries are read defensively (record guard); prototype-pollution server
|
|
14918
|
+
* names are irrelevant here because no user-controlled key or value is written —
|
|
14919
|
+
* only the three fixed boolean keys are.
|
|
14920
|
+
*/
|
|
14921
|
+
function deriveTransportAllowlist(servers) {
|
|
14922
|
+
const allowlist = {
|
|
14923
|
+
stdio: false,
|
|
14924
|
+
sse: false,
|
|
14925
|
+
http: false
|
|
14926
|
+
};
|
|
14927
|
+
for (const server of Object.values(servers)) {
|
|
14928
|
+
if (!isRecord(server)) continue;
|
|
14929
|
+
const transport = transportOf(server);
|
|
14930
|
+
if (transport) allowlist[transport] = true;
|
|
14931
|
+
}
|
|
14932
|
+
return allowlist;
|
|
14933
|
+
}
|
|
14934
|
+
//#endregion
|
|
13571
14935
|
//#region src/features/mcp/vibe-mcp.ts
|
|
13572
14936
|
const VIBE_MCP_SERVER_FIELDS = [
|
|
13573
14937
|
"transport",
|
|
@@ -14028,7 +15392,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
14028
15392
|
["goose", {
|
|
14029
15393
|
class: GooseMcp,
|
|
14030
15394
|
meta: {
|
|
14031
|
-
supportsProject:
|
|
15395
|
+
supportsProject: true,
|
|
14032
15396
|
supportsGlobal: true,
|
|
14033
15397
|
supportsEnabledTools: false,
|
|
14034
15398
|
supportsDisabledTools: false
|
|
@@ -14142,6 +15506,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
14142
15506
|
supportsDisabledTools: false
|
|
14143
15507
|
}
|
|
14144
15508
|
}],
|
|
15509
|
+
["takt", {
|
|
15510
|
+
class: TaktMcp,
|
|
15511
|
+
meta: {
|
|
15512
|
+
supportsProject: true,
|
|
15513
|
+
supportsGlobal: true,
|
|
15514
|
+
supportsEnabledTools: false,
|
|
15515
|
+
supportsDisabledTools: false
|
|
15516
|
+
}
|
|
15517
|
+
}],
|
|
14145
15518
|
["vibe", {
|
|
14146
15519
|
class: VibeMcp,
|
|
14147
15520
|
meta: {
|
|
@@ -14270,7 +15643,8 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
14270
15643
|
return await factory.class.fromRulesyncMcp({
|
|
14271
15644
|
outputRoot: this.outputRoot,
|
|
14272
15645
|
rulesyncMcp: filteredRulesyncMcp,
|
|
14273
|
-
global: this.global
|
|
15646
|
+
global: this.global,
|
|
15647
|
+
logger: this.logger
|
|
14274
15648
|
});
|
|
14275
15649
|
}));
|
|
14276
15650
|
}
|
|
@@ -14784,7 +16158,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
|
|
|
14784
16158
|
function toAntigravityCliToolName(canonical) {
|
|
14785
16159
|
return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
|
|
14786
16160
|
}
|
|
14787
|
-
function toCanonicalToolName$
|
|
16161
|
+
function toCanonicalToolName$6(cliName) {
|
|
14788
16162
|
return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
|
|
14789
16163
|
}
|
|
14790
16164
|
/**
|
|
@@ -14967,7 +16341,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
|
|
|
14967
16341
|
const processEntries = (entries, action) => {
|
|
14968
16342
|
for (const entry of entries) {
|
|
14969
16343
|
const { toolName, pattern } = parsePermissionEntry$1(entry);
|
|
14970
|
-
const canonical = toCanonicalToolName$
|
|
16344
|
+
const canonical = toCanonicalToolName$6(toolName);
|
|
14971
16345
|
if (!permission[canonical]) permission[canonical] = {};
|
|
14972
16346
|
permission[canonical][pattern] = action;
|
|
14973
16347
|
}
|
|
@@ -15006,7 +16380,7 @@ const IDE_ACTION_TO_CANONICAL = {
|
|
|
15006
16380
|
function toIdeAction(canonical) {
|
|
15007
16381
|
return CANONICAL_TO_IDE_ACTION[canonical] ?? canonical;
|
|
15008
16382
|
}
|
|
15009
|
-
function toCanonicalCategory$
|
|
16383
|
+
function toCanonicalCategory$2(ideAction) {
|
|
15010
16384
|
return IDE_ACTION_TO_CANONICAL[ideAction] ?? ideAction;
|
|
15011
16385
|
}
|
|
15012
16386
|
/**
|
|
@@ -15180,7 +16554,7 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
|
|
|
15180
16554
|
const processEntries = (entries, action) => {
|
|
15181
16555
|
for (const entry of entries) {
|
|
15182
16556
|
const { action: ideAction, pattern } = parsePermissionEntry(entry);
|
|
15183
|
-
const canonical = toCanonicalCategory$
|
|
16557
|
+
const canonical = toCanonicalCategory$2(ideAction);
|
|
15184
16558
|
if (!permission[canonical]) permission[canonical] = {};
|
|
15185
16559
|
permission[canonical][pattern] = action;
|
|
15186
16560
|
}
|
|
@@ -15229,7 +16603,7 @@ const AUGMENT_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONI
|
|
|
15229
16603
|
function toAugmentToolName(canonical) {
|
|
15230
16604
|
return CANONICAL_TO_AUGMENT_TOOL_NAMES[canonical] ?? canonical;
|
|
15231
16605
|
}
|
|
15232
|
-
function toCanonicalToolName$
|
|
16606
|
+
function toCanonicalToolName$5(augmentName) {
|
|
15233
16607
|
return AUGMENT_TO_CANONICAL_TOOL_NAMES[augmentName] ?? augmentName;
|
|
15234
16608
|
}
|
|
15235
16609
|
function actionToAugmentType(action) {
|
|
@@ -15573,7 +16947,7 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
15573
16947
|
}
|
|
15574
16948
|
const type = entry.permission.type;
|
|
15575
16949
|
if (!isBasicAugmentType(type)) continue;
|
|
15576
|
-
const canonical = toCanonicalToolName$
|
|
16950
|
+
const canonical = toCanonicalToolName$5(entry.toolName);
|
|
15577
16951
|
if (forbiddenMapKeys.has(canonical)) {
|
|
15578
16952
|
logger?.warn(`AugmentCode permissions: skipping entry for tool '${entry.toolName}' because it maps to the reserved object key '${canonical}', which cannot be used as a permission key.`);
|
|
15579
16953
|
continue;
|
|
@@ -15626,7 +17000,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
|
|
|
15626
17000
|
function toClaudeToolName(canonical) {
|
|
15627
17001
|
return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
|
|
15628
17002
|
}
|
|
15629
|
-
function toCanonicalToolName$
|
|
17003
|
+
function toCanonicalToolName$4(claudeName) {
|
|
15630
17004
|
return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
|
|
15631
17005
|
}
|
|
15632
17006
|
/**
|
|
@@ -15800,7 +17174,7 @@ function convertClaudeToRulesyncPermissions(params) {
|
|
|
15800
17174
|
const processEntries = (entries, action) => {
|
|
15801
17175
|
for (const entry of entries) {
|
|
15802
17176
|
const { toolName, pattern } = parseClaudePermissionEntry(entry);
|
|
15803
|
-
const canonical = toCanonicalToolName$
|
|
17177
|
+
const canonical = toCanonicalToolName$4(toolName);
|
|
15804
17178
|
if (!permission[canonical]) permission[canonical] = {};
|
|
15805
17179
|
permission[canonical][pattern] = action;
|
|
15806
17180
|
}
|
|
@@ -16427,7 +17801,7 @@ function toCursorPattern(canonical, pattern) {
|
|
|
16427
17801
|
}
|
|
16428
17802
|
return pattern;
|
|
16429
17803
|
}
|
|
16430
|
-
function toCanonicalCategory(cursorType, pattern) {
|
|
17804
|
+
function toCanonicalCategory$1(cursorType, pattern) {
|
|
16431
17805
|
if (cursorType === "Mcp") {
|
|
16432
17806
|
const match = pattern.match(/^([^:()]+):([^:()]+)$/);
|
|
16433
17807
|
if (match) return `${MCP_CANONICAL_PREFIX}${match[1] ?? "*"}__${match[2] ?? "*"}`;
|
|
@@ -16652,7 +18026,7 @@ function convertCursorToRulesyncPermissions(params) {
|
|
|
16652
18026
|
const processEntries = (entries, action) => {
|
|
16653
18027
|
for (const entry of entries) {
|
|
16654
18028
|
const { type, pattern } = parseCursorPermissionEntry(entry);
|
|
16655
|
-
const canonical = toCanonicalCategory(type, pattern);
|
|
18029
|
+
const canonical = toCanonicalCategory$1(type, pattern);
|
|
16656
18030
|
if (!permission[canonical]) permission[canonical] = {};
|
|
16657
18031
|
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX) ? "*" : pattern;
|
|
16658
18032
|
permission[canonical][canonicalPattern] = action;
|
|
@@ -16663,6 +18037,245 @@ function convertCursorToRulesyncPermissions(params) {
|
|
|
16663
18037
|
return { permission };
|
|
16664
18038
|
}
|
|
16665
18039
|
//#endregion
|
|
18040
|
+
//#region src/features/permissions/devin-permissions.ts
|
|
18041
|
+
/**
|
|
18042
|
+
* Mapping from rulesync canonical tool category names to Devin Local permission
|
|
18043
|
+
* scope matchers.
|
|
18044
|
+
*
|
|
18045
|
+
* Devin expresses permissions with scope-based matchers — `Read(glob)`,
|
|
18046
|
+
* `Write(glob)`, `Exec(prefix)`, and `Fetch(pattern)` — plus MCP tool patterns
|
|
18047
|
+
* (`mcp__server__tool`). The canonical `edit` and `write` categories both map
|
|
18048
|
+
* onto Devin's single `Write` scope; on import `Write` maps back to `write`, so
|
|
18049
|
+
* `edit` rules round-trip as `write` (a lossy but documented collapse). Unknown
|
|
18050
|
+
* names (e.g. `mcp__github__list_issues`) pass through verbatim.
|
|
18051
|
+
*
|
|
18052
|
+
* @see https://docs.devin.ai/cli/reference/permissions
|
|
18053
|
+
*/
|
|
18054
|
+
const CANONICAL_TO_DEVIN_SCOPE = {
|
|
18055
|
+
read: "Read",
|
|
18056
|
+
write: "Write",
|
|
18057
|
+
edit: "Write",
|
|
18058
|
+
bash: "Exec",
|
|
18059
|
+
webfetch: "Fetch"
|
|
18060
|
+
};
|
|
18061
|
+
/**
|
|
18062
|
+
* Reverse mapping from Devin scope matchers to rulesync canonical names.
|
|
18063
|
+
*/
|
|
18064
|
+
const DEVIN_SCOPE_TO_CANONICAL = {
|
|
18065
|
+
Read: "read",
|
|
18066
|
+
Write: "write",
|
|
18067
|
+
Exec: "bash",
|
|
18068
|
+
Fetch: "webfetch"
|
|
18069
|
+
};
|
|
18070
|
+
function toDevinScope(canonical) {
|
|
18071
|
+
return CANONICAL_TO_DEVIN_SCOPE[canonical] ?? canonical;
|
|
18072
|
+
}
|
|
18073
|
+
function toCanonicalCategory(devinScope) {
|
|
18074
|
+
return DEVIN_SCOPE_TO_CANONICAL[devinScope] ?? devinScope;
|
|
18075
|
+
}
|
|
18076
|
+
/**
|
|
18077
|
+
* Parse a Devin permission entry like `Read(src/**)` into scope and pattern.
|
|
18078
|
+
* Bare entries (e.g. `Read`, or a whole-tool name like `exec`) yield `*`.
|
|
18079
|
+
*/
|
|
18080
|
+
function parseDevinPermissionEntry(entry) {
|
|
18081
|
+
const parenIndex = entry.indexOf("(");
|
|
18082
|
+
if (parenIndex === -1) return {
|
|
18083
|
+
scope: entry,
|
|
18084
|
+
pattern: "*"
|
|
18085
|
+
};
|
|
18086
|
+
const scope = entry.slice(0, parenIndex);
|
|
18087
|
+
if (!entry.endsWith(")")) return {
|
|
18088
|
+
scope,
|
|
18089
|
+
pattern: "*"
|
|
18090
|
+
};
|
|
18091
|
+
return {
|
|
18092
|
+
scope,
|
|
18093
|
+
pattern: entry.slice(parenIndex + 1, -1) || "*"
|
|
18094
|
+
};
|
|
18095
|
+
}
|
|
18096
|
+
/**
|
|
18097
|
+
* Build a Devin permission entry like `Read(src/**)`. A `*` pattern collapses to
|
|
18098
|
+
* the bare scope (`Read`), matching the whole scope.
|
|
18099
|
+
*/
|
|
18100
|
+
function buildDevinPermissionEntry(scope, pattern) {
|
|
18101
|
+
if (pattern === "*") return scope;
|
|
18102
|
+
return `${scope}(${pattern})`;
|
|
18103
|
+
}
|
|
18104
|
+
/**
|
|
18105
|
+
* Permissions generator for Devin Local (native `.devin/` configuration).
|
|
18106
|
+
*
|
|
18107
|
+
* Maps rulesync permission actions onto Devin's `permissions` block inside its
|
|
18108
|
+
* native config file — `allow` / `deny` / `ask` arrays of scope matchers
|
|
18109
|
+
* (`Read(glob)`, `Write(glob)`, `Exec(prefix)`, `Fetch(pattern)`, plus
|
|
18110
|
+
* `mcp__server__tool` patterns). Devin evaluates the arrays with strict
|
|
18111
|
+
* precedence: `deny` is checked before `ask`, which is checked before `allow`,
|
|
18112
|
+
* so a deny rule always wins.
|
|
18113
|
+
*
|
|
18114
|
+
* - Project scope: `.devin/config.json`
|
|
18115
|
+
* - Global scope: `~/.config/devin/config.json`
|
|
18116
|
+
*
|
|
18117
|
+
* The config file is shared with the MCP (`mcpServers`) and, in global mode, the
|
|
18118
|
+
* hooks (`hooks`) features, so reads and writes merge into the existing JSON and
|
|
18119
|
+
* the file is never deleted; only the managed `permissions` key is rewritten.
|
|
18120
|
+
*
|
|
18121
|
+
* @see https://docs.devin.ai/cli/reference/permissions
|
|
18122
|
+
*/
|
|
18123
|
+
var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
18124
|
+
constructor(params) {
|
|
18125
|
+
super({
|
|
18126
|
+
...params,
|
|
18127
|
+
fileContent: params.fileContent ?? "{}"
|
|
18128
|
+
});
|
|
18129
|
+
}
|
|
18130
|
+
/**
|
|
18131
|
+
* config.json may carry the MCP/hooks features' keys, so it is never deleted;
|
|
18132
|
+
* only the managed `permissions` key is rewritten.
|
|
18133
|
+
*/
|
|
18134
|
+
isDeletable() {
|
|
18135
|
+
return false;
|
|
18136
|
+
}
|
|
18137
|
+
static getSettablePaths({ global = false } = {}) {
|
|
18138
|
+
if (global) return {
|
|
18139
|
+
relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
|
|
18140
|
+
relativeFilePath: DEVIN_CONFIG_FILE_NAME
|
|
18141
|
+
};
|
|
18142
|
+
return {
|
|
18143
|
+
relativeDirPath: DEVIN_DIR,
|
|
18144
|
+
relativeFilePath: DEVIN_CONFIG_FILE_NAME
|
|
18145
|
+
};
|
|
18146
|
+
}
|
|
18147
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
18148
|
+
const paths = DevinPermissions.getSettablePaths({ global });
|
|
18149
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"permissions\":{}}";
|
|
18150
|
+
return new DevinPermissions({
|
|
18151
|
+
outputRoot,
|
|
18152
|
+
relativeDirPath: paths.relativeDirPath,
|
|
18153
|
+
relativeFilePath: paths.relativeFilePath,
|
|
18154
|
+
fileContent,
|
|
18155
|
+
validate
|
|
18156
|
+
});
|
|
18157
|
+
}
|
|
18158
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, validate = true }) {
|
|
18159
|
+
const paths = DevinPermissions.getSettablePaths({ global });
|
|
18160
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
18161
|
+
const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
|
|
18162
|
+
let settings;
|
|
18163
|
+
try {
|
|
18164
|
+
const parsed = JSON.parse(existingContent);
|
|
18165
|
+
settings = isRecord(parsed) ? parsed : {};
|
|
18166
|
+
} catch (error) {
|
|
18167
|
+
throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
18168
|
+
}
|
|
18169
|
+
const config = rulesyncPermissions.getJson();
|
|
18170
|
+
const { allow, ask, deny } = convertRulesyncToDevinPermissions(config);
|
|
18171
|
+
const managedScopes = new Set(Object.keys(config.permission).map((category) => toDevinScope(category)));
|
|
18172
|
+
const existingPermissions = isRecord(settings.permissions) ? settings.permissions : {};
|
|
18173
|
+
const preserve = (entries) => (entries ?? []).filter((entry) => !managedScopes.has(parseDevinPermissionEntry(entry).scope));
|
|
18174
|
+
const mergedAllow = uniq([...preserve(existingPermissions.allow), ...allow].toSorted());
|
|
18175
|
+
const mergedAsk = uniq([...preserve(existingPermissions.ask), ...ask].toSorted());
|
|
18176
|
+
const mergedDeny = uniq([...preserve(existingPermissions.deny), ...deny].toSorted());
|
|
18177
|
+
const mergedPermissions = { ...existingPermissions };
|
|
18178
|
+
if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
|
|
18179
|
+
else delete mergedPermissions.allow;
|
|
18180
|
+
if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
|
|
18181
|
+
else delete mergedPermissions.ask;
|
|
18182
|
+
if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
|
|
18183
|
+
else delete mergedPermissions.deny;
|
|
18184
|
+
const merged = {
|
|
18185
|
+
...settings,
|
|
18186
|
+
permissions: mergedPermissions
|
|
18187
|
+
};
|
|
18188
|
+
return new DevinPermissions({
|
|
18189
|
+
outputRoot,
|
|
18190
|
+
relativeDirPath: paths.relativeDirPath,
|
|
18191
|
+
relativeFilePath: paths.relativeFilePath,
|
|
18192
|
+
fileContent: JSON.stringify(merged, null, 2),
|
|
18193
|
+
validate
|
|
18194
|
+
});
|
|
18195
|
+
}
|
|
18196
|
+
toRulesyncPermissions() {
|
|
18197
|
+
let settings;
|
|
18198
|
+
try {
|
|
18199
|
+
const parsed = JSON.parse(this.getFileContent());
|
|
18200
|
+
settings = isRecord(parsed) ? parsed : {};
|
|
18201
|
+
} catch (error) {
|
|
18202
|
+
throw new Error(`Failed to parse Devin permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18203
|
+
}
|
|
18204
|
+
const permissions = isRecord(settings.permissions) ? settings.permissions : {};
|
|
18205
|
+
const config = convertDevinToRulesyncPermissions({
|
|
18206
|
+
allow: Array.isArray(permissions.allow) ? permissions.allow : [],
|
|
18207
|
+
ask: Array.isArray(permissions.ask) ? permissions.ask : [],
|
|
18208
|
+
deny: Array.isArray(permissions.deny) ? permissions.deny : []
|
|
18209
|
+
});
|
|
18210
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
|
|
18211
|
+
}
|
|
18212
|
+
validate() {
|
|
18213
|
+
return {
|
|
18214
|
+
success: true,
|
|
18215
|
+
error: null
|
|
18216
|
+
};
|
|
18217
|
+
}
|
|
18218
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
18219
|
+
return new DevinPermissions({
|
|
18220
|
+
outputRoot,
|
|
18221
|
+
relativeDirPath,
|
|
18222
|
+
relativeFilePath,
|
|
18223
|
+
fileContent: JSON.stringify({ permissions: {} }, null, 2),
|
|
18224
|
+
validate: false
|
|
18225
|
+
});
|
|
18226
|
+
}
|
|
18227
|
+
};
|
|
18228
|
+
/**
|
|
18229
|
+
* Convert rulesync permissions config to Devin allow/ask/deny arrays.
|
|
18230
|
+
*/
|
|
18231
|
+
function convertRulesyncToDevinPermissions(config) {
|
|
18232
|
+
const allow = [];
|
|
18233
|
+
const ask = [];
|
|
18234
|
+
const deny = [];
|
|
18235
|
+
for (const [category, rules] of Object.entries(config.permission)) {
|
|
18236
|
+
const scope = toDevinScope(category);
|
|
18237
|
+
for (const [pattern, action] of Object.entries(rules)) {
|
|
18238
|
+
const entry = buildDevinPermissionEntry(scope, pattern);
|
|
18239
|
+
switch (action) {
|
|
18240
|
+
case "allow":
|
|
18241
|
+
allow.push(entry);
|
|
18242
|
+
break;
|
|
18243
|
+
case "ask":
|
|
18244
|
+
ask.push(entry);
|
|
18245
|
+
break;
|
|
18246
|
+
case "deny":
|
|
18247
|
+
deny.push(entry);
|
|
18248
|
+
break;
|
|
18249
|
+
}
|
|
18250
|
+
}
|
|
18251
|
+
}
|
|
18252
|
+
return {
|
|
18253
|
+
allow,
|
|
18254
|
+
ask,
|
|
18255
|
+
deny
|
|
18256
|
+
};
|
|
18257
|
+
}
|
|
18258
|
+
/**
|
|
18259
|
+
* Convert Devin allow/ask/deny arrays to rulesync permissions config. Entries
|
|
18260
|
+
* are applied allow → ask → deny so the most restrictive action wins for a
|
|
18261
|
+
* given (scope, pattern), mirroring Devin's deny > ask > allow precedence.
|
|
18262
|
+
*/
|
|
18263
|
+
function convertDevinToRulesyncPermissions(params) {
|
|
18264
|
+
const permission = {};
|
|
18265
|
+
const processEntries = (entries, action) => {
|
|
18266
|
+
for (const entry of entries) {
|
|
18267
|
+
const { scope, pattern } = parseDevinPermissionEntry(entry);
|
|
18268
|
+
if (isPrototypePollutionKey(scope) || isPrototypePollutionKey(pattern)) continue;
|
|
18269
|
+
const canonical = toCanonicalCategory(scope);
|
|
18270
|
+
(permission[canonical] ??= {})[pattern] = action;
|
|
18271
|
+
}
|
|
18272
|
+
};
|
|
18273
|
+
processEntries(params.allow, "allow");
|
|
18274
|
+
processEntries(params.ask, "ask");
|
|
18275
|
+
processEntries(params.deny, "deny");
|
|
18276
|
+
return { permission };
|
|
18277
|
+
}
|
|
18278
|
+
//#endregion
|
|
16666
18279
|
//#region src/features/permissions/factorydroid-permissions.ts
|
|
16667
18280
|
/**
|
|
16668
18281
|
* Permissions adapter for Factory Droid.
|
|
@@ -16922,7 +18535,7 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
|
|
|
16922
18535
|
} catch (error) {
|
|
16923
18536
|
throw new Error(`Failed to parse existing Goose permission.yaml at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
16924
18537
|
}
|
|
16925
|
-
const config = isRecord
|
|
18538
|
+
const config = isRecord(parsed) ? { ...parsed } : {};
|
|
16926
18539
|
config[GOOSE_USER_KEY] = convertRulesyncToGoosePermissionConfig({
|
|
16927
18540
|
config: rulesyncPermissions.getJson(),
|
|
16928
18541
|
logger
|
|
@@ -16944,8 +18557,8 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
|
|
|
16944
18557
|
} catch (error) {
|
|
16945
18558
|
throw new Error(`Failed to parse Goose permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
16946
18559
|
}
|
|
16947
|
-
const config = isRecord
|
|
16948
|
-
const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord
|
|
18560
|
+
const config = isRecord(parsed) ? parsed : {};
|
|
18561
|
+
const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
|
|
16949
18562
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
16950
18563
|
}
|
|
16951
18564
|
validate() {
|
|
@@ -17085,7 +18698,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
17085
18698
|
throw new Error(`Failed to parse existing Grok config.toml at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
17086
18699
|
}
|
|
17087
18700
|
const mode = deriveGrokPermissionMode(rulesyncPermissions.getJson());
|
|
17088
|
-
const existingUi = isRecord
|
|
18701
|
+
const existingUi = isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
|
|
17089
18702
|
parsed[GROKCLI_UI_KEY] = {
|
|
17090
18703
|
...existingUi,
|
|
17091
18704
|
[GROKCLI_PERMISSION_MODE_KEY]: mode
|
|
@@ -17107,7 +18720,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
17107
18720
|
} catch (error) {
|
|
17108
18721
|
throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
17109
18722
|
}
|
|
17110
|
-
const action = (isRecord
|
|
18723
|
+
const action = (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
|
|
17111
18724
|
const rulesyncConfig = { permission: { bash: { [CATCH_ALL_PATTERN$2]: action } } };
|
|
17112
18725
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
17113
18726
|
}
|
|
@@ -17189,6 +18802,205 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
17189
18802
|
}
|
|
17190
18803
|
};
|
|
17191
18804
|
//#endregion
|
|
18805
|
+
//#region src/features/permissions/junie-permissions.ts
|
|
18806
|
+
/**
|
|
18807
|
+
* JetBrains Junie CLI Action Allowlist (`allowlist.json`).
|
|
18808
|
+
*
|
|
18809
|
+
* Junie gates actions through an allowlist evaluated top-to-bottom (first match
|
|
18810
|
+
* wins). Project scope lives in `.junie/allowlist.json`; user scope lives in
|
|
18811
|
+
* `~/.junie/allowlist.json`.
|
|
18812
|
+
*
|
|
18813
|
+
* ```json
|
|
18814
|
+
* {
|
|
18815
|
+
* "defaultBehavior": "ask",
|
|
18816
|
+
* "allowReadonlyCommands": true,
|
|
18817
|
+
* "rules": {
|
|
18818
|
+
* "executables": [ { "prefix": "git ", "action": "allow" } ],
|
|
18819
|
+
* "fileEditing": [ { "pattern": "src/**", "action": "allow" } ],
|
|
18820
|
+
* "mcpTools": [ { "prefix": "search", "action": "allow" } ],
|
|
18821
|
+
* "readOutsideProject":[ { "pattern": "/etc/**", "action": "deny" } ]
|
|
18822
|
+
* }
|
|
18823
|
+
* }
|
|
18824
|
+
* ```
|
|
18825
|
+
*
|
|
18826
|
+
* Each rule carries a literal `prefix` (matches commands that start with it) or
|
|
18827
|
+
* a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action`
|
|
18828
|
+
* (`allow` | `ask` | `deny`). rulesync's canonical actions map 1:1 onto Junie's.
|
|
18829
|
+
*
|
|
18830
|
+
* Category mapping (rulesync canonical <-> Junie rule group):
|
|
18831
|
+
* - `bash` <-> `executables`
|
|
18832
|
+
* - `edit`/`write` -> `fileEditing` (imported back as `edit`)
|
|
18833
|
+
* - `read` <-> `readOutsideProject`
|
|
18834
|
+
* - `mcp` <-> `mcpTools`
|
|
18835
|
+
*
|
|
18836
|
+
* Categories Junie cannot represent (e.g. `webfetch`) are skipped on export
|
|
18837
|
+
* (with a warning when they carry rules). The top-level `defaultBehavior` and
|
|
18838
|
+
* `allowReadonlyCommands` settings have no canonical equivalent: they are
|
|
18839
|
+
* preserved verbatim on export but not imported into the rulesync model.
|
|
18840
|
+
*
|
|
18841
|
+
* @see https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html
|
|
18842
|
+
*/
|
|
18843
|
+
const JUNIE_RULE_GROUPS = [
|
|
18844
|
+
"executables",
|
|
18845
|
+
"fileEditing",
|
|
18846
|
+
"mcpTools",
|
|
18847
|
+
"readOutsideProject"
|
|
18848
|
+
];
|
|
18849
|
+
const CANONICAL_TO_JUNIE_GROUP = {
|
|
18850
|
+
bash: "executables",
|
|
18851
|
+
edit: "fileEditing",
|
|
18852
|
+
write: "fileEditing",
|
|
18853
|
+
read: "readOutsideProject",
|
|
18854
|
+
mcp: "mcpTools"
|
|
18855
|
+
};
|
|
18856
|
+
const JUNIE_GROUP_TO_CANONICAL = {
|
|
18857
|
+
executables: "bash",
|
|
18858
|
+
fileEditing: "edit",
|
|
18859
|
+
mcpTools: "mcp",
|
|
18860
|
+
readOutsideProject: "read"
|
|
18861
|
+
};
|
|
18862
|
+
const JUNIE_DEFAULT_BEHAVIOR = "ask";
|
|
18863
|
+
function isPermissionAction$1(value) {
|
|
18864
|
+
return PermissionActionSchema.safeParse(value).success;
|
|
18865
|
+
}
|
|
18866
|
+
/**
|
|
18867
|
+
* Whether a rulesync pattern uses glob syntax. Junie expresses literal
|
|
18868
|
+
* "starts-with" matches as `prefix` and glob matches as `pattern`, so a pattern
|
|
18869
|
+
* containing any glob metacharacter (`*`, `?`, `[`) is emitted as `pattern`.
|
|
18870
|
+
*/
|
|
18871
|
+
function isGlobPattern(pattern) {
|
|
18872
|
+
return /[*?[]/.test(pattern);
|
|
18873
|
+
}
|
|
18874
|
+
var JuniePermissions = class JuniePermissions extends ToolPermissions {
|
|
18875
|
+
constructor(params) {
|
|
18876
|
+
super({
|
|
18877
|
+
...params,
|
|
18878
|
+
fileContent: params.fileContent ?? "{}"
|
|
18879
|
+
});
|
|
18880
|
+
}
|
|
18881
|
+
isDeletable() {
|
|
18882
|
+
return false;
|
|
18883
|
+
}
|
|
18884
|
+
static getSettablePaths(_options = {}) {
|
|
18885
|
+
return {
|
|
18886
|
+
relativeDirPath: JUNIE_DIR,
|
|
18887
|
+
relativeFilePath: JUNIE_PERMISSIONS_FILE_NAME
|
|
18888
|
+
};
|
|
18889
|
+
}
|
|
18890
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
18891
|
+
const paths = JuniePermissions.getSettablePaths({ global });
|
|
18892
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
|
|
18893
|
+
return new JuniePermissions({
|
|
18894
|
+
outputRoot,
|
|
18895
|
+
relativeDirPath: paths.relativeDirPath,
|
|
18896
|
+
relativeFilePath: paths.relativeFilePath,
|
|
18897
|
+
fileContent,
|
|
18898
|
+
validate
|
|
18899
|
+
});
|
|
18900
|
+
}
|
|
18901
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
18902
|
+
const paths = JuniePermissions.getSettablePaths({ global });
|
|
18903
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
18904
|
+
const existingContent = await readOrInitializeFileContent(filePath, "{}");
|
|
18905
|
+
let existing;
|
|
18906
|
+
try {
|
|
18907
|
+
const parsed = JSON.parse(existingContent);
|
|
18908
|
+
existing = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
18909
|
+
} catch (error) {
|
|
18910
|
+
throw new Error(`Failed to parse existing Junie allowlist at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
18911
|
+
}
|
|
18912
|
+
const rules = convertRulesyncToJunieRules({
|
|
18913
|
+
config: rulesyncPermissions.getJson(),
|
|
18914
|
+
logger
|
|
18915
|
+
});
|
|
18916
|
+
const merged = {
|
|
18917
|
+
...existing,
|
|
18918
|
+
defaultBehavior: existing.defaultBehavior ?? JUNIE_DEFAULT_BEHAVIOR,
|
|
18919
|
+
rules
|
|
18920
|
+
};
|
|
18921
|
+
return new JuniePermissions({
|
|
18922
|
+
outputRoot,
|
|
18923
|
+
relativeDirPath: paths.relativeDirPath,
|
|
18924
|
+
relativeFilePath: paths.relativeFilePath,
|
|
18925
|
+
fileContent: JSON.stringify(merged, null, 2),
|
|
18926
|
+
validate: true
|
|
18927
|
+
});
|
|
18928
|
+
}
|
|
18929
|
+
toRulesyncPermissions() {
|
|
18930
|
+
let allowlist;
|
|
18931
|
+
try {
|
|
18932
|
+
const parsed = JSON.parse(this.getFileContent());
|
|
18933
|
+
allowlist = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
18934
|
+
} catch (error) {
|
|
18935
|
+
throw new Error(`Failed to parse Junie permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18936
|
+
}
|
|
18937
|
+
const config = convertJunieToRulesyncPermissions({ allowlist });
|
|
18938
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
|
|
18939
|
+
}
|
|
18940
|
+
validate() {
|
|
18941
|
+
return {
|
|
18942
|
+
success: true,
|
|
18943
|
+
error: null
|
|
18944
|
+
};
|
|
18945
|
+
}
|
|
18946
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
18947
|
+
return new JuniePermissions({
|
|
18948
|
+
outputRoot,
|
|
18949
|
+
relativeDirPath,
|
|
18950
|
+
relativeFilePath,
|
|
18951
|
+
fileContent: "{}",
|
|
18952
|
+
validate: false
|
|
18953
|
+
});
|
|
18954
|
+
}
|
|
18955
|
+
};
|
|
18956
|
+
/**
|
|
18957
|
+
* Convert rulesync permissions config into Junie's `rules` object. Categories
|
|
18958
|
+
* with no Junie rule group (e.g. `webfetch`) are skipped, with a warning when
|
|
18959
|
+
* they carry any rule so the gap is surfaced.
|
|
18960
|
+
*/
|
|
18961
|
+
function convertRulesyncToJunieRules({ config, logger }) {
|
|
18962
|
+
const rules = {};
|
|
18963
|
+
for (const [category, patterns] of Object.entries(config.permission)) {
|
|
18964
|
+
const group = CANONICAL_TO_JUNIE_GROUP[category];
|
|
18965
|
+
if (!group) {
|
|
18966
|
+
if (Object.keys(patterns).length > 0) logger?.warn(`Junie allowlist only models executables/fileEditing/mcpTools/readOutsideProject (canonical bash/edit/write/read/mcp); '${category}' rules cannot be represented and were skipped.`);
|
|
18967
|
+
continue;
|
|
18968
|
+
}
|
|
18969
|
+
for (const [pattern, action] of Object.entries(patterns)) {
|
|
18970
|
+
const rule = isGlobPattern(pattern) ? {
|
|
18971
|
+
pattern,
|
|
18972
|
+
action
|
|
18973
|
+
} : {
|
|
18974
|
+
prefix: pattern,
|
|
18975
|
+
action
|
|
18976
|
+
};
|
|
18977
|
+
(rules[group] ??= []).push(rule);
|
|
18978
|
+
}
|
|
18979
|
+
}
|
|
18980
|
+
return rules;
|
|
18981
|
+
}
|
|
18982
|
+
/**
|
|
18983
|
+
* Convert a Junie allowlist back into rulesync permissions config. The
|
|
18984
|
+
* top-level `defaultBehavior` / `allowReadonlyCommands` settings have no
|
|
18985
|
+
* canonical equivalent and are not imported.
|
|
18986
|
+
*/
|
|
18987
|
+
function convertJunieToRulesyncPermissions({ allowlist }) {
|
|
18988
|
+
const permission = {};
|
|
18989
|
+
const rules = allowlist.rules;
|
|
18990
|
+
if (rules && typeof rules === "object") for (const group of JUNIE_RULE_GROUPS) {
|
|
18991
|
+
const list = rules[group];
|
|
18992
|
+
if (!Array.isArray(list)) continue;
|
|
18993
|
+
const category = JUNIE_GROUP_TO_CANONICAL[group];
|
|
18994
|
+
for (const rule of list) {
|
|
18995
|
+
if (!rule || typeof rule !== "object") continue;
|
|
18996
|
+
const pattern = typeof rule.pattern === "string" ? rule.pattern : typeof rule.prefix === "string" ? rule.prefix : void 0;
|
|
18997
|
+
if (pattern === void 0 || !isPermissionAction$1(rule.action)) continue;
|
|
18998
|
+
(permission[category] ??= {})[pattern] = rule.action;
|
|
18999
|
+
}
|
|
19000
|
+
}
|
|
19001
|
+
return { permission };
|
|
19002
|
+
}
|
|
19003
|
+
//#endregion
|
|
17192
19004
|
//#region src/features/permissions/kilo-permissions.ts
|
|
17193
19005
|
const KiloPermissionSchema = z.union([z.enum([
|
|
17194
19006
|
"allow",
|
|
@@ -17537,7 +19349,11 @@ const OpencodePermissionSchema = z.union([z.enum([
|
|
|
17537
19349
|
"ask",
|
|
17538
19350
|
"deny"
|
|
17539
19351
|
]))]);
|
|
17540
|
-
const OpencodePermissionsConfigSchema = z.looseObject({ permission: z.optional(z.
|
|
19352
|
+
const OpencodePermissionsConfigSchema = z.looseObject({ permission: z.optional(z.union([z.enum([
|
|
19353
|
+
"allow",
|
|
19354
|
+
"ask",
|
|
19355
|
+
"deny"
|
|
19356
|
+
]), z.record(z.string(), OpencodePermissionSchema)])) });
|
|
17541
19357
|
var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
17542
19358
|
json;
|
|
17543
19359
|
constructor(params) {
|
|
@@ -17640,6 +19456,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
17640
19456
|
}
|
|
17641
19457
|
normalizePermission(permission) {
|
|
17642
19458
|
if (!permission) return {};
|
|
19459
|
+
if (typeof permission === "string") return { "*": { "*": permission } };
|
|
17643
19460
|
return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
|
|
17644
19461
|
}
|
|
17645
19462
|
};
|
|
@@ -17676,7 +19493,7 @@ const QWEN_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL
|
|
|
17676
19493
|
function toQwenToolName(canonical) {
|
|
17677
19494
|
return CANONICAL_TO_QWEN_TOOL_NAMES[canonical] ?? canonical;
|
|
17678
19495
|
}
|
|
17679
|
-
function toCanonicalToolName$
|
|
19496
|
+
function toCanonicalToolName$3(qwenName) {
|
|
17680
19497
|
return QWEN_TO_CANONICAL_TOOL_NAMES[qwenName] ?? qwenName;
|
|
17681
19498
|
}
|
|
17682
19499
|
function parseQwenPermissionEntry(entry, options = {}) {
|
|
@@ -17870,13 +19687,242 @@ function convertQwenToRulesyncPermissions(params) {
|
|
|
17870
19687
|
const parsed = parseQwenPermissionEntry(entry, { logger });
|
|
17871
19688
|
if (!parsed.ok) {
|
|
17872
19689
|
if (action === "deny") {
|
|
17873
|
-
const canonical = toCanonicalToolName$
|
|
19690
|
+
const canonical = toCanonicalToolName$3(parsed.toolName);
|
|
17874
19691
|
if (!permission[canonical]) permission[canonical] = {};
|
|
17875
19692
|
permission[canonical]["*"] = action;
|
|
17876
19693
|
}
|
|
17877
19694
|
continue;
|
|
17878
19695
|
}
|
|
17879
19696
|
const { toolName, pattern } = parsed;
|
|
19697
|
+
const canonical = toCanonicalToolName$3(toolName);
|
|
19698
|
+
if (!permission[canonical]) permission[canonical] = {};
|
|
19699
|
+
permission[canonical][pattern] = action;
|
|
19700
|
+
}
|
|
19701
|
+
};
|
|
19702
|
+
processEntries(params.allow, "allow");
|
|
19703
|
+
processEntries(params.ask, "ask");
|
|
19704
|
+
processEntries(params.deny, "deny");
|
|
19705
|
+
return { permission };
|
|
19706
|
+
}
|
|
19707
|
+
//#endregion
|
|
19708
|
+
//#region src/features/permissions/reasonix-permissions.ts
|
|
19709
|
+
/**
|
|
19710
|
+
* Mapping from rulesync canonical tool category names (lowercase) to Reasonix
|
|
19711
|
+
* permission-rule tool families (PascalCase).
|
|
19712
|
+
*
|
|
19713
|
+
* Reasonix's `[permissions]` rule syntax (SPEC.md §3.7) is explicitly
|
|
19714
|
+
* documented as "Claude Code-style": "Bash and file mutation approvals use
|
|
19715
|
+
* Claude Code-style families such as `Bash(npm run build)`, `Bash(npm run
|
|
19716
|
+
* test:*)`, and `Edit(docs/**)`." Reasonix also accepts legacy lowercase tool
|
|
19717
|
+
* IDs for compatibility, but new rules are saved using these PascalCase
|
|
19718
|
+
* families, so rulesync reuses the same mapping `claudecode-permissions.ts`
|
|
19719
|
+
* uses (the closest documented precedent for this syntax).
|
|
19720
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
|
|
19721
|
+
*/
|
|
19722
|
+
const CANONICAL_TO_REASONIX_TOOL_NAMES = {
|
|
19723
|
+
bash: "Bash",
|
|
19724
|
+
read: "Read",
|
|
19725
|
+
edit: "Edit",
|
|
19726
|
+
write: "Write",
|
|
19727
|
+
webfetch: "WebFetch",
|
|
19728
|
+
websearch: "WebSearch",
|
|
19729
|
+
grep: "Grep",
|
|
19730
|
+
glob: "Glob",
|
|
19731
|
+
notebookedit: "NotebookEdit",
|
|
19732
|
+
agent: "Agent"
|
|
19733
|
+
};
|
|
19734
|
+
/**
|
|
19735
|
+
* Reverse mapping from Reasonix tool names to rulesync canonical names.
|
|
19736
|
+
*/
|
|
19737
|
+
const REASONIX_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
19738
|
+
function toReasonixToolName(canonical) {
|
|
19739
|
+
return CANONICAL_TO_REASONIX_TOOL_NAMES[canonical] ?? canonical;
|
|
19740
|
+
}
|
|
19741
|
+
function toCanonicalToolName$2(reasonixName) {
|
|
19742
|
+
return REASONIX_TO_CANONICAL_TOOL_NAMES[reasonixName] ?? reasonixName;
|
|
19743
|
+
}
|
|
19744
|
+
/**
|
|
19745
|
+
* Parse a Reasonix permission entry like "Bash(npm run *)" into tool name and pattern.
|
|
19746
|
+
* If no parentheses, returns the tool name with "*" as the pattern.
|
|
19747
|
+
*/
|
|
19748
|
+
function parseReasonixPermissionEntry(entry) {
|
|
19749
|
+
const parenIndex = entry.indexOf("(");
|
|
19750
|
+
if (parenIndex === -1) return {
|
|
19751
|
+
toolName: entry,
|
|
19752
|
+
pattern: "*"
|
|
19753
|
+
};
|
|
19754
|
+
const toolName = entry.slice(0, parenIndex);
|
|
19755
|
+
if (!entry.endsWith(")")) return {
|
|
19756
|
+
toolName,
|
|
19757
|
+
pattern: "*"
|
|
19758
|
+
};
|
|
19759
|
+
return {
|
|
19760
|
+
toolName,
|
|
19761
|
+
pattern: entry.slice(parenIndex + 1, -1) || "*"
|
|
19762
|
+
};
|
|
19763
|
+
}
|
|
19764
|
+
/**
|
|
19765
|
+
* Build a Reasonix permission entry like "Bash(npm run *)".
|
|
19766
|
+
* If the pattern is "*", returns just the tool name.
|
|
19767
|
+
*/
|
|
19768
|
+
function buildReasonixPermissionEntry(toolName, pattern) {
|
|
19769
|
+
if (pattern === "*") return toolName;
|
|
19770
|
+
return `${toolName}(${pattern})`;
|
|
19771
|
+
}
|
|
19772
|
+
function parseReasonixConfig(fileContent) {
|
|
19773
|
+
const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
|
|
19774
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
19775
|
+
return { ...parsed };
|
|
19776
|
+
}
|
|
19777
|
+
function toStringArray$1(value) {
|
|
19778
|
+
if (!Array.isArray(value)) return [];
|
|
19779
|
+
return value.filter((entry) => typeof entry === "string");
|
|
19780
|
+
}
|
|
19781
|
+
function toPermissionsTable(value) {
|
|
19782
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
19783
|
+
return { ...value };
|
|
19784
|
+
}
|
|
19785
|
+
var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
19786
|
+
toml;
|
|
19787
|
+
constructor(params) {
|
|
19788
|
+
super(params);
|
|
19789
|
+
this.toml = parseReasonixConfig(this.getFileContent());
|
|
19790
|
+
}
|
|
19791
|
+
isDeletable() {
|
|
19792
|
+
return false;
|
|
19793
|
+
}
|
|
19794
|
+
static getSettablePaths({ global } = {}) {
|
|
19795
|
+
if (global) return {
|
|
19796
|
+
relativeDirPath: REASONIX_GLOBAL_DIR,
|
|
19797
|
+
relativeFilePath: REASONIX_GLOBAL_PERMISSIONS_FILE_NAME
|
|
19798
|
+
};
|
|
19799
|
+
return {
|
|
19800
|
+
relativeDirPath: ".",
|
|
19801
|
+
relativeFilePath: REASONIX_PROJECT_PERMISSIONS_FILE_NAME
|
|
19802
|
+
};
|
|
19803
|
+
}
|
|
19804
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
19805
|
+
const paths = this.getSettablePaths({ global });
|
|
19806
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({});
|
|
19807
|
+
return new ReasonixPermissions({
|
|
19808
|
+
outputRoot,
|
|
19809
|
+
relativeDirPath: paths.relativeDirPath,
|
|
19810
|
+
relativeFilePath: paths.relativeFilePath,
|
|
19811
|
+
fileContent,
|
|
19812
|
+
validate
|
|
19813
|
+
});
|
|
19814
|
+
}
|
|
19815
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
|
|
19816
|
+
const paths = this.getSettablePaths({ global });
|
|
19817
|
+
const parsed = parseReasonixConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
|
|
19818
|
+
const config = rulesyncPermissions.getJson();
|
|
19819
|
+
const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
|
|
19820
|
+
const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
|
|
19821
|
+
const existingPermissions = toPermissionsTable(parsed.permissions);
|
|
19822
|
+
const preservedAllow = toStringArray$1(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
|
|
19823
|
+
const preservedAsk = toStringArray$1(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
|
|
19824
|
+
const preservedDeny = toStringArray$1(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
|
|
19825
|
+
if (logger && managedToolNames.has("Read")) {
|
|
19826
|
+
const droppedReadDenyEntries = toStringArray$1(existingPermissions.deny).filter((entry) => {
|
|
19827
|
+
const { toolName } = parseReasonixPermissionEntry(entry);
|
|
19828
|
+
return toolName === "Read";
|
|
19829
|
+
});
|
|
19830
|
+
if (droppedReadDenyEntries.length > 0) logger.warn(`Permissions feature manages 'Read' tool and will overwrite ${droppedReadDenyEntries.length} existing Read deny entries (possibly from ignore feature). Permissions take precedence.`);
|
|
19831
|
+
}
|
|
19832
|
+
const mergedPermissions = { ...existingPermissions };
|
|
19833
|
+
const mergedAllow = uniq([...preservedAllow, ...allow].toSorted());
|
|
19834
|
+
const mergedAsk = uniq([...preservedAsk, ...ask].toSorted());
|
|
19835
|
+
const mergedDeny = uniq([...preservedDeny, ...deny].toSorted());
|
|
19836
|
+
if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
|
|
19837
|
+
else delete mergedPermissions.allow;
|
|
19838
|
+
if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
|
|
19839
|
+
else delete mergedPermissions.ask;
|
|
19840
|
+
if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
|
|
19841
|
+
else delete mergedPermissions.deny;
|
|
19842
|
+
const merged = {
|
|
19843
|
+
...parsed,
|
|
19844
|
+
permissions: mergedPermissions
|
|
19845
|
+
};
|
|
19846
|
+
const fileContent = smolToml.stringify(merged);
|
|
19847
|
+
return new ReasonixPermissions({
|
|
19848
|
+
outputRoot,
|
|
19849
|
+
relativeDirPath: paths.relativeDirPath,
|
|
19850
|
+
relativeFilePath: paths.relativeFilePath,
|
|
19851
|
+
fileContent,
|
|
19852
|
+
validate
|
|
19853
|
+
});
|
|
19854
|
+
}
|
|
19855
|
+
toRulesyncPermissions() {
|
|
19856
|
+
const permissions = toPermissionsTable(this.toml.permissions);
|
|
19857
|
+
const config = convertReasonixToRulesyncPermissions({
|
|
19858
|
+
allow: toStringArray$1(permissions.allow),
|
|
19859
|
+
ask: toStringArray$1(permissions.ask),
|
|
19860
|
+
deny: toStringArray$1(permissions.deny)
|
|
19861
|
+
});
|
|
19862
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
|
|
19863
|
+
}
|
|
19864
|
+
validate() {
|
|
19865
|
+
try {
|
|
19866
|
+
parseReasonixConfig(this.getFileContent());
|
|
19867
|
+
return {
|
|
19868
|
+
success: true,
|
|
19869
|
+
error: null
|
|
19870
|
+
};
|
|
19871
|
+
} catch (error) {
|
|
19872
|
+
return {
|
|
19873
|
+
success: false,
|
|
19874
|
+
error: /* @__PURE__ */ new Error(`Failed to parse Reasonix config TOML: ${formatError(error)}`)
|
|
19875
|
+
};
|
|
19876
|
+
}
|
|
19877
|
+
}
|
|
19878
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
19879
|
+
return new ReasonixPermissions({
|
|
19880
|
+
outputRoot,
|
|
19881
|
+
relativeDirPath,
|
|
19882
|
+
relativeFilePath,
|
|
19883
|
+
fileContent: smolToml.stringify({}),
|
|
19884
|
+
validate: false
|
|
19885
|
+
});
|
|
19886
|
+
}
|
|
19887
|
+
};
|
|
19888
|
+
/**
|
|
19889
|
+
* Convert rulesync permissions config to Reasonix allow/ask/deny arrays.
|
|
19890
|
+
*/
|
|
19891
|
+
function convertRulesyncToReasonixPermissions(config) {
|
|
19892
|
+
const allow = [];
|
|
19893
|
+
const ask = [];
|
|
19894
|
+
const deny = [];
|
|
19895
|
+
for (const [category, rules] of Object.entries(config.permission)) {
|
|
19896
|
+
const reasonixToolName = toReasonixToolName(category);
|
|
19897
|
+
for (const [pattern, action] of Object.entries(rules)) {
|
|
19898
|
+
const entry = buildReasonixPermissionEntry(reasonixToolName, pattern);
|
|
19899
|
+
switch (action) {
|
|
19900
|
+
case "allow":
|
|
19901
|
+
allow.push(entry);
|
|
19902
|
+
break;
|
|
19903
|
+
case "ask":
|
|
19904
|
+
ask.push(entry);
|
|
19905
|
+
break;
|
|
19906
|
+
case "deny":
|
|
19907
|
+
deny.push(entry);
|
|
19908
|
+
break;
|
|
19909
|
+
}
|
|
19910
|
+
}
|
|
19911
|
+
}
|
|
19912
|
+
return {
|
|
19913
|
+
allow,
|
|
19914
|
+
ask,
|
|
19915
|
+
deny
|
|
19916
|
+
};
|
|
19917
|
+
}
|
|
19918
|
+
/**
|
|
19919
|
+
* Convert Reasonix allow/ask/deny arrays to rulesync permissions config.
|
|
19920
|
+
*/
|
|
19921
|
+
function convertReasonixToRulesyncPermissions(params) {
|
|
19922
|
+
const permission = {};
|
|
19923
|
+
const processEntries = (entries, action) => {
|
|
19924
|
+
for (const entry of entries) {
|
|
19925
|
+
const { toolName, pattern } = parseReasonixPermissionEntry(entry);
|
|
17880
19926
|
const canonical = toCanonicalToolName$2(toolName);
|
|
17881
19927
|
if (!permission[canonical]) permission[canonical] = {};
|
|
17882
19928
|
permission[canonical][pattern] = action;
|
|
@@ -17989,13 +20035,13 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
17989
20035
|
} catch (error) {
|
|
17990
20036
|
throw new Error(`Failed to parse existing Rovodev config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
17991
20037
|
}
|
|
17992
|
-
const config = isRecord
|
|
20038
|
+
const config = isRecord(parsed) ? { ...parsed } : {};
|
|
17993
20039
|
const toolPermissions = convertRulesyncToRovodevToolPermissions({
|
|
17994
20040
|
config: rulesyncPermissions.getJson(),
|
|
17995
20041
|
logger
|
|
17996
20042
|
});
|
|
17997
20043
|
config.toolPermissions = {
|
|
17998
|
-
...isRecord
|
|
20044
|
+
...isRecord(config.toolPermissions) ? { ...config.toolPermissions } : {},
|
|
17999
20045
|
...toolPermissions
|
|
18000
20046
|
};
|
|
18001
20047
|
return new RovodevPermissions({
|
|
@@ -18015,8 +20061,8 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
18015
20061
|
} catch (error) {
|
|
18016
20062
|
throw new Error(`Failed to parse Rovodev permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18017
20063
|
}
|
|
18018
|
-
const config = isRecord
|
|
18019
|
-
const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord
|
|
20064
|
+
const config = isRecord(parsed) ? parsed : {};
|
|
20065
|
+
const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord(config.toolPermissions) ? config.toolPermissions : {});
|
|
18020
20066
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
18021
20067
|
}
|
|
18022
20068
|
validate() {
|
|
@@ -18094,11 +20140,11 @@ function convertBashRules(rules) {
|
|
|
18094
20140
|
function convertRovodevToolPermissionsToRulesync(toolPermissions) {
|
|
18095
20141
|
const permission = {};
|
|
18096
20142
|
const bash = toolPermissions.bash;
|
|
18097
|
-
if (isRecord
|
|
20143
|
+
if (isRecord(bash)) {
|
|
18098
20144
|
const bashRules = {};
|
|
18099
20145
|
if (isPermissionAction(bash.default)) bashRules[CATCH_ALL_PATTERN$1] = bash.default;
|
|
18100
20146
|
if (Array.isArray(bash.commands)) {
|
|
18101
|
-
for (const entry of bash.commands) if (isRecord
|
|
20147
|
+
for (const entry of bash.commands) if (isRecord(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
|
|
18102
20148
|
}
|
|
18103
20149
|
if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
|
|
18104
20150
|
}
|
|
@@ -18237,21 +20283,6 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
|
18237
20283
|
}
|
|
18238
20284
|
};
|
|
18239
20285
|
/**
|
|
18240
|
-
* Parse a Takt `config.yaml` into a plain object, treating an empty file as `{}`.
|
|
18241
|
-
*/
|
|
18242
|
-
function parseTaktConfig(fileContent, relativeDirPath, relativeFilePath) {
|
|
18243
|
-
const configPath = join(relativeDirPath, relativeFilePath);
|
|
18244
|
-
let parsed;
|
|
18245
|
-
try {
|
|
18246
|
-
parsed = fileContent.trim() === "" ? {} : load(fileContent);
|
|
18247
|
-
} catch (error) {
|
|
18248
|
-
throw new Error(`Failed to parse Takt config at ${configPath}: ${formatError(error)}`, { cause: error });
|
|
18249
|
-
}
|
|
18250
|
-
if (parsed === void 0 || parsed === null) return {};
|
|
18251
|
-
if (!isPlainObject(parsed)) throw new Error(`Failed to parse Takt config at ${configPath}: expected a YAML mapping`);
|
|
18252
|
-
return parsed;
|
|
18253
|
-
}
|
|
18254
|
-
/**
|
|
18255
20286
|
* Resolve the active Takt provider: the top-level `provider:` value, else the
|
|
18256
20287
|
* sole key in `provider_profiles`, else the `claude` default.
|
|
18257
20288
|
*/
|
|
@@ -18573,8 +20604,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
18573
20604
|
config: rulesyncPermissions.getJson(),
|
|
18574
20605
|
logger
|
|
18575
20606
|
});
|
|
18576
|
-
const agents = isRecord
|
|
18577
|
-
const profiles = isRecord
|
|
20607
|
+
const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
|
|
20608
|
+
const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
|
|
18578
20609
|
const mergedAllow = uniq(allow.toSorted());
|
|
18579
20610
|
const mergedDeny = uniq(deny.toSorted());
|
|
18580
20611
|
if (mergedAllow.length > 0) profiles[ALLOWLIST_KEY] = mergedAllow;
|
|
@@ -18599,8 +20630,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
18599
20630
|
} catch (error) {
|
|
18600
20631
|
throw new Error(`Failed to parse Warp permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18601
20632
|
}
|
|
18602
|
-
const agents = isRecord
|
|
18603
|
-
const profiles = isRecord
|
|
20633
|
+
const agents = isRecord(settings.agents) ? settings.agents : {};
|
|
20634
|
+
const profiles = isRecord(agents.profiles) ? agents.profiles : {};
|
|
18604
20635
|
const config = convertWarpToRulesyncPermissions({
|
|
18605
20636
|
allow: isStringArray(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
|
|
18606
20637
|
deny: isStringArray(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
|
|
@@ -18951,6 +20982,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
18951
20982
|
supportsImport: true
|
|
18952
20983
|
}
|
|
18953
20984
|
}],
|
|
20985
|
+
["devin", {
|
|
20986
|
+
class: DevinPermissions,
|
|
20987
|
+
meta: {
|
|
20988
|
+
supportsProject: true,
|
|
20989
|
+
supportsGlobal: true,
|
|
20990
|
+
supportsImport: true
|
|
20991
|
+
}
|
|
20992
|
+
}],
|
|
18954
20993
|
["factorydroid", {
|
|
18955
20994
|
class: FactorydroidPermissions,
|
|
18956
20995
|
meta: {
|
|
@@ -18983,6 +21022,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
18983
21022
|
supportsImport: true
|
|
18984
21023
|
}
|
|
18985
21024
|
}],
|
|
21025
|
+
["junie", {
|
|
21026
|
+
class: JuniePermissions,
|
|
21027
|
+
meta: {
|
|
21028
|
+
supportsProject: true,
|
|
21029
|
+
supportsGlobal: true,
|
|
21030
|
+
supportsImport: true
|
|
21031
|
+
}
|
|
21032
|
+
}],
|
|
18986
21033
|
["kilo", {
|
|
18987
21034
|
class: KiloPermissions,
|
|
18988
21035
|
meta: {
|
|
@@ -19031,6 +21078,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
19031
21078
|
supportsImport: true
|
|
19032
21079
|
}
|
|
19033
21080
|
}],
|
|
21081
|
+
["reasonix", {
|
|
21082
|
+
class: ReasonixPermissions,
|
|
21083
|
+
meta: {
|
|
21084
|
+
supportsProject: true,
|
|
21085
|
+
supportsGlobal: true,
|
|
21086
|
+
supportsImport: true
|
|
21087
|
+
}
|
|
21088
|
+
}],
|
|
19034
21089
|
["rovodev", {
|
|
19035
21090
|
class: RovodevPermissions,
|
|
19036
21091
|
meta: {
|
|
@@ -19583,7 +21638,7 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
19583
21638
|
opencode: z.optional(z.looseObject({
|
|
19584
21639
|
"allowed-tools": z.optional(z.array(z.string())),
|
|
19585
21640
|
license: z.optional(z.string()),
|
|
19586
|
-
compatibility: z.optional(z.looseObject({})),
|
|
21641
|
+
compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
|
|
19587
21642
|
metadata: z.optional(z.looseObject({}))
|
|
19588
21643
|
})),
|
|
19589
21644
|
kilo: z.optional(z.looseObject({
|
|
@@ -19604,7 +21659,8 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
19604
21659
|
})),
|
|
19605
21660
|
copilotcli: z.optional(z.looseObject({
|
|
19606
21661
|
license: z.optional(z.string()),
|
|
19607
|
-
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
|
|
21662
|
+
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
|
|
21663
|
+
"argument-hint": z.optional(z.string())
|
|
19608
21664
|
})),
|
|
19609
21665
|
pi: z.optional(z.looseObject({
|
|
19610
21666
|
"allowed-tools": z.optional(z.array(z.string())),
|
|
@@ -21358,7 +23414,11 @@ const CopilotSkillFrontmatterSchema = z.looseObject({
|
|
|
21358
23414
|
});
|
|
21359
23415
|
/**
|
|
21360
23416
|
* Represents a GitHub Copilot skill directory.
|
|
21361
|
-
*
|
|
23417
|
+
*
|
|
23418
|
+
* Copilot discovers project skills from `.github/skills/` and personal/global
|
|
23419
|
+
* skills from `~/.copilot/skills/`. Each skill is a directory containing a
|
|
23420
|
+
* `SKILL.md` file with `name`/`description` frontmatter.
|
|
23421
|
+
* https://docs.github.com/en/copilot/concepts/agents/about-agent-skills
|
|
21362
23422
|
*/
|
|
21363
23423
|
var CopilotSkill = class CopilotSkill extends ToolSkill {
|
|
21364
23424
|
constructor({ outputRoot = process.cwd(), relativeDirPath = COPILOT_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
@@ -21380,7 +23440,7 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
|
|
|
21380
23440
|
}
|
|
21381
23441
|
}
|
|
21382
23442
|
static getSettablePaths(options) {
|
|
21383
|
-
if (options?.global)
|
|
23443
|
+
if (options?.global) return { relativeDirPath: COPILOT_SKILLS_GLOBAL_DIR_PATH };
|
|
21384
23444
|
return { relativeDirPath: COPILOT_SKILLS_DIR_PATH };
|
|
21385
23445
|
}
|
|
21386
23446
|
getFrontmatter() {
|
|
@@ -21495,7 +23555,8 @@ const CopilotcliSkillFrontmatterSchema = z.looseObject({
|
|
|
21495
23555
|
name: z.string(),
|
|
21496
23556
|
description: z.string(),
|
|
21497
23557
|
license: z.optional(z.string()),
|
|
21498
|
-
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
|
|
23558
|
+
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
|
|
23559
|
+
"argument-hint": z.optional(z.string())
|
|
21499
23560
|
});
|
|
21500
23561
|
/**
|
|
21501
23562
|
* Represents a GitHub Copilot CLI skill directory.
|
|
@@ -21525,7 +23586,7 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
21525
23586
|
}
|
|
21526
23587
|
}
|
|
21527
23588
|
static getSettablePaths(options) {
|
|
21528
|
-
if (options?.global) return { relativeDirPath:
|
|
23589
|
+
if (options?.global) return { relativeDirPath: COPILOT_SKILLS_GLOBAL_DIR_PATH };
|
|
21529
23590
|
return { relativeDirPath: COPILOT_SKILLS_DIR_PATH };
|
|
21530
23591
|
}
|
|
21531
23592
|
getFrontmatter() {
|
|
@@ -21553,7 +23614,8 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
21553
23614
|
const frontmatter = this.getFrontmatter();
|
|
21554
23615
|
const copilotcliSection = {
|
|
21555
23616
|
...frontmatter.license !== void 0 && { license: frontmatter.license },
|
|
21556
|
-
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
|
|
23617
|
+
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
|
|
23618
|
+
...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] }
|
|
21557
23619
|
};
|
|
21558
23620
|
const rulesyncFrontmatter = {
|
|
21559
23621
|
name: frontmatter.name,
|
|
@@ -21579,7 +23641,8 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
21579
23641
|
name: rulesyncFrontmatter.name,
|
|
21580
23642
|
description: rulesyncFrontmatter.description,
|
|
21581
23643
|
...rulesyncFrontmatter.copilotcli?.license !== void 0 && { license: rulesyncFrontmatter.copilotcli.license },
|
|
21582
|
-
...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] }
|
|
23644
|
+
...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] },
|
|
23645
|
+
...rulesyncFrontmatter.copilotcli?.["argument-hint"] !== void 0 && { "argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"] }
|
|
21583
23646
|
};
|
|
21584
23647
|
return new CopilotcliSkill({
|
|
21585
23648
|
outputRoot,
|
|
@@ -22805,8 +24868,7 @@ var KiroSkill = class KiroSkill extends ToolSkill {
|
|
|
22805
24868
|
if (!result.success) throw result.error;
|
|
22806
24869
|
}
|
|
22807
24870
|
}
|
|
22808
|
-
static getSettablePaths(
|
|
22809
|
-
if (options?.global) throw new Error("KiroSkill does not support global mode.");
|
|
24871
|
+
static getSettablePaths(_options) {
|
|
22810
24872
|
return { relativeDirPath: KIRO_SKILLS_DIR_PATH };
|
|
22811
24873
|
}
|
|
22812
24874
|
getFrontmatter() {
|
|
@@ -22948,10 +25010,19 @@ const OpenCodeSkillFrontmatterSchema = z.looseObject({
|
|
|
22948
25010
|
name: z.string(),
|
|
22949
25011
|
description: z.string(),
|
|
22950
25012
|
license: z.optional(z.string()),
|
|
22951
|
-
compatibility: z.optional(z.looseObject({})),
|
|
25013
|
+
compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
|
|
22952
25014
|
metadata: z.optional(z.looseObject({})),
|
|
22953
25015
|
"allowed-tools": z.optional(z.array(z.string()))
|
|
22954
25016
|
});
|
|
25017
|
+
/**
|
|
25018
|
+
* Reads a top-level `compatibility` value from rulesync frontmatter, accepting
|
|
25019
|
+
* both the documented string form (e.g. `compatibility: opencode`) and the
|
|
25020
|
+
* legacy object form. Returns `undefined` for any other shape.
|
|
25021
|
+
*/
|
|
25022
|
+
function readTopLevelCompatibility(value) {
|
|
25023
|
+
if (typeof value === "string") return value;
|
|
25024
|
+
if (typeof value === "object" && value !== null) return value;
|
|
25025
|
+
}
|
|
22955
25026
|
var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
22956
25027
|
constructor({ outputRoot = process.cwd(), relativeDirPath = OPENCODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
22957
25028
|
super({
|
|
@@ -23028,7 +25099,7 @@ var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
|
23028
25099
|
const opencodeSection = rulesyncFrontmatter.opencode;
|
|
23029
25100
|
const looseTopLevel = rulesyncFrontmatter;
|
|
23030
25101
|
const topLevelLicense = typeof looseTopLevel.license === "string" ? looseTopLevel.license : void 0;
|
|
23031
|
-
const topLevelCompatibility =
|
|
25102
|
+
const topLevelCompatibility = readTopLevelCompatibility(looseTopLevel.compatibility);
|
|
23032
25103
|
const topLevelMetadata = typeof looseTopLevel.metadata === "object" && looseTopLevel.metadata !== null ? looseTopLevel.metadata : void 0;
|
|
23033
25104
|
const license = opencodeSection?.license ?? topLevelLicense;
|
|
23034
25105
|
const compatibility = opencodeSection?.compatibility ?? topLevelCompatibility;
|
|
@@ -24382,7 +26453,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
24382
26453
|
meta: {
|
|
24383
26454
|
supportsProject: true,
|
|
24384
26455
|
supportsSimulated: false,
|
|
24385
|
-
supportsGlobal:
|
|
26456
|
+
supportsGlobal: true
|
|
24386
26457
|
}
|
|
24387
26458
|
}],
|
|
24388
26459
|
["copilotcli", {
|
|
@@ -24470,7 +26541,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
24470
26541
|
meta: {
|
|
24471
26542
|
supportsProject: true,
|
|
24472
26543
|
supportsSimulated: false,
|
|
24473
|
-
supportsGlobal:
|
|
26544
|
+
supportsGlobal: true
|
|
24474
26545
|
}
|
|
24475
26546
|
}],
|
|
24476
26547
|
["kiro-ide", {
|
|
@@ -24478,7 +26549,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
24478
26549
|
meta: {
|
|
24479
26550
|
supportsProject: true,
|
|
24480
26551
|
supportsSimulated: false,
|
|
24481
|
-
supportsGlobal:
|
|
26552
|
+
supportsGlobal: true
|
|
24482
26553
|
}
|
|
24483
26554
|
}],
|
|
24484
26555
|
["opencode", {
|
|
@@ -28566,7 +30637,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
28566
30637
|
class: KiroCliSubagent,
|
|
28567
30638
|
meta: {
|
|
28568
30639
|
supportsSimulated: false,
|
|
28569
|
-
supportsGlobal:
|
|
30640
|
+
supportsGlobal: true,
|
|
28570
30641
|
filePattern: "*.json"
|
|
28571
30642
|
}
|
|
28572
30643
|
}],
|
|
@@ -28574,7 +30645,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
28574
30645
|
class: KiroIdeSubagent,
|
|
28575
30646
|
meta: {
|
|
28576
30647
|
supportsSimulated: false,
|
|
28577
|
-
supportsGlobal:
|
|
30648
|
+
supportsGlobal: true,
|
|
28578
30649
|
filePattern: "*.md"
|
|
28579
30650
|
}
|
|
28580
30651
|
}],
|
|
@@ -31111,7 +33182,7 @@ const STRATEGIES = [
|
|
|
31111
33182
|
* plus companion `globs`/`description` fields. (`.devin/rules/` is the
|
|
31112
33183
|
* pre-rebrand legacy location the tool still reads.)
|
|
31113
33184
|
* - Global scope: a single plain-markdown, always-on file (no frontmatter) at
|
|
31114
|
-
* `~/.
|
|
33185
|
+
* `~/.config/devin/AGENTS.md` (Devin Local global always-on rules).
|
|
31115
33186
|
*
|
|
31116
33187
|
* Trigger inference (when no explicit devin trigger is persisted):
|
|
31117
33188
|
* - Specific globs (non wildcard) → glob
|
|
@@ -31138,8 +33209,8 @@ var DevinRule = class DevinRule extends ToolRule {
|
|
|
31138
33209
|
}
|
|
31139
33210
|
static getGlobalRootPath(excludeToolDir) {
|
|
31140
33211
|
return {
|
|
31141
|
-
relativeDirPath: buildToolPath(
|
|
31142
|
-
relativeFilePath:
|
|
33212
|
+
relativeDirPath: buildToolPath(DEVIN_GLOBAL_CONFIG_DIR_PATH, ".", excludeToolDir),
|
|
33213
|
+
relativeFilePath: DEVIN_GLOBAL_AGENTS_FILE_NAME
|
|
31143
33214
|
};
|
|
31144
33215
|
}
|
|
31145
33216
|
static getSettablePaths({ global = false, excludeToolDir } = {}) {
|
|
@@ -31702,6 +33773,9 @@ var KiloRule = class KiloRule extends ToolRule {
|
|
|
31702
33773
|
nonRoot: { relativeDirPath: buildToolPath(KILO_DIR, KILO_RULES_DIR_NAME, excludeToolDir) }
|
|
31703
33774
|
};
|
|
31704
33775
|
}
|
|
33776
|
+
static getExtraSharedWritePaths({ global = false } = {}) {
|
|
33777
|
+
return global ? [] : [KiloMcp.getSettablePaths({ global: false })];
|
|
33778
|
+
}
|
|
31705
33779
|
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
31706
33780
|
const paths = this.getSettablePaths({ global });
|
|
31707
33781
|
if (relativeFilePath === paths.root.relativeFilePath) {
|
|
@@ -31963,6 +34037,9 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
|
|
|
31963
34037
|
nonRoot: { relativeDirPath: buildToolPath(OPENCODE_DIR, "memories", excludeToolDir) }
|
|
31964
34038
|
};
|
|
31965
34039
|
}
|
|
34040
|
+
static getExtraSharedWritePaths({ global = false } = {}) {
|
|
34041
|
+
return global ? [] : [OpencodeMcp.getSettablePaths({ global: false })];
|
|
34042
|
+
}
|
|
31966
34043
|
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
31967
34044
|
const paths = this.getSettablePaths({ global });
|
|
31968
34045
|
if (relativeFilePath === paths.root.relativeFilePath) {
|
|
@@ -32029,7 +34106,7 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
|
|
|
32029
34106
|
/**
|
|
32030
34107
|
* Rule generator for Pi Coding Agent.
|
|
32031
34108
|
*
|
|
32032
|
-
* Pi loads instruction context
|
|
34109
|
+
* Pi loads instruction context from the `AGENTS.md` / `CLAUDE.md` family —
|
|
32033
34110
|
* the global `~/.pi/agent/AGENTS.md` plus files discovered by walking up the
|
|
32034
34111
|
* directory tree from the current working directory. It does NOT resolve
|
|
32035
34112
|
* `@`-imports or a TOON file list, and has no `.agents/memories/` concept, so
|
|
@@ -32040,6 +34117,15 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
|
|
|
32040
34117
|
* to map onto; their bodies are folded into the single root `AGENTS.md` by the
|
|
32041
34118
|
* RulesProcessor (there is no separate non-root output location — `nonRoot` is
|
|
32042
34119
|
* `undefined`). This mirrors the codexcli, grokcli, warp, and deepagents targets.
|
|
34120
|
+
*
|
|
34121
|
+
* Pi also loads two system-prompt instruction files that rulesync does NOT emit:
|
|
34122
|
+
* `.pi/SYSTEM.md` (global `~/.pi/agent/SYSTEM.md`) *replaces* the default system
|
|
34123
|
+
* prompt entirely, and `.pi/APPEND_SYSTEM.md` (global
|
|
34124
|
+
* `~/.pi/agent/APPEND_SYSTEM.md`) *appends* to it. rulesync's rules model only
|
|
34125
|
+
* routes a designated `root` rule to a single context file and has no convention
|
|
34126
|
+
* for marking a rule as "replace" vs "append" the system prompt, so these
|
|
34127
|
+
* surfaces are documented in docs/reference/file-formats.md and left to be
|
|
34128
|
+
* authored by hand rather than mapped to a speculative new frontmatter flag.
|
|
32043
34129
|
*/
|
|
32044
34130
|
var PiRule = class PiRule extends ToolRule {
|
|
32045
34131
|
constructor({ fileContent, root, ...rest }) {
|
|
@@ -32630,6 +34716,37 @@ var RovodevRule = class RovodevRule extends ToolRule {
|
|
|
32630
34716
|
toolTarget: "rovodev"
|
|
32631
34717
|
});
|
|
32632
34718
|
}
|
|
34719
|
+
/**
|
|
34720
|
+
* Mirror the primary `.rovodev/AGENTS.md` root rule to `./AGENTS.md` so project
|
|
34721
|
+
* memory stays discoverable at the repo root. Empty when `rootRule` is not the primary root.
|
|
34722
|
+
*/
|
|
34723
|
+
static getRootMirrorFiles({ outputRoot, rootRule, content }) {
|
|
34724
|
+
if (!(rootRule instanceof RovodevRule)) return [];
|
|
34725
|
+
const primary = this.getSettablePaths({ global: false }).root;
|
|
34726
|
+
if (rootRule.getRelativeDirPath() !== primary.relativeDirPath || rootRule.getRelativeFilePath() !== primary.relativeFilePath) return [];
|
|
34727
|
+
return [new RovodevRule({
|
|
34728
|
+
outputRoot,
|
|
34729
|
+
relativeDirPath: ".",
|
|
34730
|
+
relativeFilePath: ROVODEV_RULE_FILE_NAME,
|
|
34731
|
+
fileContent: content,
|
|
34732
|
+
validate: true,
|
|
34733
|
+
root: true
|
|
34734
|
+
})];
|
|
34735
|
+
}
|
|
34736
|
+
/**
|
|
34737
|
+
* Globs for mirror deletion: the `./AGENTS.md` mirror (`mirrorGlob`) is deleted
|
|
34738
|
+
* only when the primary `.rovodev/AGENTS.md` (`primaryGlob`) still exists.
|
|
34739
|
+
*/
|
|
34740
|
+
static getRootMirrorDeletionGlobs({ outputRoot }) {
|
|
34741
|
+
return {
|
|
34742
|
+
primaryGlob: join(outputRoot, ROVODEV_DIR, ROVODEV_RULE_FILE_NAME),
|
|
34743
|
+
mirrorGlob: join(outputRoot, ROVODEV_RULE_FILE_NAME)
|
|
34744
|
+
};
|
|
34745
|
+
}
|
|
34746
|
+
/** Glob for the `separate-local-file` deletion; rovodev writes it at project root, not under `.rovodev/`. */
|
|
34747
|
+
static getLocalRootDeletionGlob({ outputRoot, fileName }) {
|
|
34748
|
+
return join(outputRoot, fileName);
|
|
34749
|
+
}
|
|
32633
34750
|
};
|
|
32634
34751
|
//#endregion
|
|
32635
34752
|
//#region src/features/rules/takt-rule.ts
|
|
@@ -33352,7 +35469,7 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
33352
35469
|
});
|
|
33353
35470
|
this.applyRootRuleSections({
|
|
33354
35471
|
toolRules,
|
|
33355
|
-
|
|
35472
|
+
factory
|
|
33356
35473
|
});
|
|
33357
35474
|
return [...toolRules, ...extraFiles];
|
|
33358
35475
|
}
|
|
@@ -33414,31 +35531,16 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
33414
35531
|
* reference and conventions sections to the root rule content. Mutates the
|
|
33415
35532
|
* root rule in place.
|
|
33416
35533
|
*/
|
|
33417
|
-
applyRootRuleSections({ toolRules,
|
|
35534
|
+
applyRootRuleSections({ toolRules, factory }) {
|
|
35535
|
+
const { meta } = factory;
|
|
33418
35536
|
const rootRule = toolRules.find((rule) => rule.isRoot());
|
|
33419
35537
|
if (!rootRule) return;
|
|
33420
35538
|
const newContent = this.generateReferenceSectionFromMeta(meta, toolRules) + (!meta.createsSeparateConventionsRule && meta.additionalConventions ? this.generateAdditionalConventionsSectionFromMeta(meta) : "") + rootRule.getFileContent();
|
|
33421
35539
|
rootRule.setFileContent(newContent);
|
|
33422
|
-
if (meta.mirrorsRootToAgentsMd && !this.global)
|
|
33423
|
-
|
|
35540
|
+
if (meta.mirrorsRootToAgentsMd && !this.global && factory.class.getRootMirrorFiles) toolRules.push(...factory.class.getRootMirrorFiles({
|
|
35541
|
+
outputRoot: this.outputRoot,
|
|
33424
35542
|
rootRule,
|
|
33425
35543
|
content: newContent
|
|
33426
|
-
});
|
|
33427
|
-
}
|
|
33428
|
-
/**
|
|
33429
|
-
* Mirror the primary root rule to a project-root `AGENTS.md` for tools whose
|
|
33430
|
-
* primary root lives in a subdirectory (rovodev: `.rovodev/AGENTS.md`).
|
|
33431
|
-
*/
|
|
33432
|
-
mirrorRootRuleToAgentsMd({ toolRules, rootRule, content }) {
|
|
33433
|
-
if (!(rootRule instanceof RovodevRule)) return;
|
|
33434
|
-
const primary = RovodevRule.getSettablePaths({ global: false }).root;
|
|
33435
|
-
if (rootRule.getRelativeDirPath() === primary.relativeDirPath && rootRule.getRelativeFilePath() === primary.relativeFilePath) toolRules.push(new RovodevRule({
|
|
33436
|
-
outputRoot: this.outputRoot,
|
|
33437
|
-
relativeDirPath: ".",
|
|
33438
|
-
relativeFilePath: "AGENTS.md",
|
|
33439
|
-
fileContent: content,
|
|
33440
|
-
validate: true,
|
|
33441
|
-
root: true
|
|
33442
35544
|
}));
|
|
33443
35545
|
}
|
|
33444
35546
|
buildSkillList(skillClass) {
|
|
@@ -33664,15 +35766,19 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
33664
35766
|
const localRootToolRules = await (async () => {
|
|
33665
35767
|
if (!forDeletion || this.global || factory.meta.localRootMode !== "separate-local-file" || !factory.meta.localRootFileName) return [];
|
|
33666
35768
|
const fileName = factory.meta.localRootFileName;
|
|
33667
|
-
if (factory.class
|
|
35769
|
+
if (factory.class.getLocalRootDeletionGlob) return buildDeletionRulesFromPaths(await findFilesByGlobs(factory.class.getLocalRootDeletionGlob({
|
|
35770
|
+
outputRoot: this.outputRoot,
|
|
35771
|
+
fileName
|
|
35772
|
+
})));
|
|
33668
35773
|
if (!settablePaths.root) return [];
|
|
33669
35774
|
return buildDeletionRulesFromPaths(await findFilesWithFallback(join(this.outputRoot, settablePaths.root.relativeDirPath ?? ".", fileName), settablePaths.alternativeRoots, (alt) => join(this.outputRoot, alt.relativeDirPath, fileName)));
|
|
33670
35775
|
})();
|
|
33671
35776
|
this.logger.debug(`Found ${localRootToolRules.length} local root tool rule files for deletion`);
|
|
33672
35777
|
const rootMirrorDeletionRules = await (async () => {
|
|
33673
|
-
if (!forDeletion || this.global || !factory.meta.mirrorsRootToAgentsMd || factory.class
|
|
33674
|
-
|
|
33675
|
-
|
|
35778
|
+
if (!forDeletion || this.global || !factory.meta.mirrorsRootToAgentsMd || !factory.class.getRootMirrorDeletionGlobs) return [];
|
|
35779
|
+
const { primaryGlob, mirrorGlob } = factory.class.getRootMirrorDeletionGlobs({ outputRoot: this.outputRoot });
|
|
35780
|
+
if ((await findFilesByGlobs(primaryGlob)).length === 0) return [];
|
|
35781
|
+
return buildDeletionRulesFromPaths(await findFilesByGlobs(mirrorGlob));
|
|
33676
35782
|
})();
|
|
33677
35783
|
const nonRootToolRules = await (async () => {
|
|
33678
35784
|
if (!settablePaths.nonRoot) return [];
|
|
@@ -34121,63 +36227,228 @@ function warnUnsupportedTargets(params) {
|
|
|
34121
36227
|
async function checkRulesyncDirExists(params) {
|
|
34122
36228
|
return fileExists(join(params.inputRoot, RULESYNC_RELATIVE_DIR_PATH));
|
|
34123
36229
|
}
|
|
36230
|
+
function dependsOnReachable(byId, from, target) {
|
|
36231
|
+
const seen = /* @__PURE__ */ new Set();
|
|
36232
|
+
const stack = [from];
|
|
36233
|
+
while (stack.length > 0) {
|
|
36234
|
+
const current = stack.pop();
|
|
36235
|
+
if (current === void 0 || seen.has(current)) continue;
|
|
36236
|
+
seen.add(current);
|
|
36237
|
+
if (current === target) return true;
|
|
36238
|
+
for (const dep of byId.get(current)?.dependsOn ?? []) stack.push(dep);
|
|
36239
|
+
}
|
|
36240
|
+
return false;
|
|
36241
|
+
}
|
|
36242
|
+
function assertSharedFilesOrdered(steps, byId) {
|
|
36243
|
+
const writersByFile = /* @__PURE__ */ new Map();
|
|
36244
|
+
for (const step of steps) for (const file of step.writesSharedFile ?? []) writersByFile.set(file, [...writersByFile.get(file) ?? [], step.id]);
|
|
36245
|
+
for (const [file, writers] of writersByFile) for (let i = 0; i < writers.length; i++) for (let j = i + 1; j < writers.length; j++) {
|
|
36246
|
+
const a = writers[i];
|
|
36247
|
+
const b = writers[j];
|
|
36248
|
+
if (!dependsOnReachable(byId, a, b) && !dependsOnReachable(byId, b, a)) throw new Error(`Generation steps '${a}' and '${b}' both write the shared file '${file}' but neither declares a 'dependsOn' the other. Add a 'dependsOn' so the read-modify-write order is fixed; otherwise one step silently drops the other's keys.`);
|
|
36249
|
+
}
|
|
36250
|
+
}
|
|
36251
|
+
/**
|
|
36252
|
+
* Topologically sort generation steps and reject ordering hazards: a shared file
|
|
36253
|
+
* with two writers not ordered by `dependsOn` (a silent data-loss trap), an
|
|
36254
|
+
* unknown dependency, or a cycle. Reordering `steps` stays safe as a result.
|
|
36255
|
+
*
|
|
36256
|
+
* @throws Error if a shared file has unordered writers, a dependency is unknown,
|
|
36257
|
+
* or the dependency graph contains a cycle.
|
|
36258
|
+
*/
|
|
36259
|
+
function resolveExecutionOrder(steps) {
|
|
36260
|
+
const byId = new Map(steps.map((step) => [step.id, step]));
|
|
36261
|
+
assertSharedFilesOrdered(steps, byId);
|
|
36262
|
+
const unresolvedDeps = new Map(steps.map((step) => [step.id, 0]));
|
|
36263
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
36264
|
+
for (const step of steps) for (const dep of step.dependsOn ?? []) {
|
|
36265
|
+
if (!byId.has(dep)) throw new Error(`Generation step '${step.id}' depends on unknown step '${dep}'.`);
|
|
36266
|
+
unresolvedDeps.set(step.id, (unresolvedDeps.get(step.id) ?? 0) + 1);
|
|
36267
|
+
dependents.set(dep, [...dependents.get(dep) ?? [], step.id]);
|
|
36268
|
+
}
|
|
36269
|
+
const ready = steps.filter((step) => (unresolvedDeps.get(step.id) ?? 0) === 0).map((step) => step.id);
|
|
36270
|
+
const ordered = [];
|
|
36271
|
+
while (ready.length > 0) {
|
|
36272
|
+
const id = ready.shift();
|
|
36273
|
+
ordered.push(byId.get(id));
|
|
36274
|
+
for (const dependent of dependents.get(id) ?? []) {
|
|
36275
|
+
const next = (unresolvedDeps.get(dependent) ?? 0) - 1;
|
|
36276
|
+
unresolvedDeps.set(dependent, next);
|
|
36277
|
+
if (next === 0) ready.push(dependent);
|
|
36278
|
+
}
|
|
36279
|
+
}
|
|
36280
|
+
if (ordered.length !== steps.length) throw new Error("Generation steps contain a cyclic 'dependsOn' dependency.");
|
|
36281
|
+
return ordered;
|
|
36282
|
+
}
|
|
36283
|
+
/**
|
|
36284
|
+
* The static shape of the generation step graph: which steps write which shared
|
|
36285
|
+
* (read-modify-write) config files, and the `dependsOn` edges that fix a safe order
|
|
36286
|
+
* for those writers. Exported (separately from the `run` closures, which need a
|
|
36287
|
+
* live `config`/`logger`) so `resolveExecutionOrder`'s ordering guarantee can be
|
|
36288
|
+
* tested directly against the real graph rather than a hand-copied one. Readonly
|
|
36289
|
+
* so a consumer can't mutate this module-level singleton and affect every
|
|
36290
|
+
* subsequent `generate()` call in the process.
|
|
36291
|
+
*/
|
|
36292
|
+
const GENERATION_STEP_GRAPH = [
|
|
36293
|
+
{
|
|
36294
|
+
id: "ignore",
|
|
36295
|
+
writesSharedFile: [".claude/settings.json", ".zed/settings.json"]
|
|
36296
|
+
},
|
|
36297
|
+
{
|
|
36298
|
+
id: "mcp",
|
|
36299
|
+
writesSharedFile: [
|
|
36300
|
+
".amp/settings.json",
|
|
36301
|
+
".augment/settings.json",
|
|
36302
|
+
".codex/config.toml",
|
|
36303
|
+
".config/amp/settings.json",
|
|
36304
|
+
".config/devin/config.json",
|
|
36305
|
+
".config/opencode/opencode.json",
|
|
36306
|
+
".config/zed/settings.json",
|
|
36307
|
+
".devin/config.json",
|
|
36308
|
+
".grok/config.toml",
|
|
36309
|
+
".hermes/config.yaml",
|
|
36310
|
+
".qwen/settings.json",
|
|
36311
|
+
".reasonix/config.toml",
|
|
36312
|
+
".takt/config.yaml",
|
|
36313
|
+
".vibe/config.toml",
|
|
36314
|
+
".zed/settings.json",
|
|
36315
|
+
"kilo.json",
|
|
36316
|
+
"opencode.json",
|
|
36317
|
+
"reasonix.toml"
|
|
36318
|
+
],
|
|
36319
|
+
dependsOn: ["ignore"]
|
|
36320
|
+
},
|
|
36321
|
+
{ id: "commands" },
|
|
36322
|
+
{ id: "subagents" },
|
|
36323
|
+
{ id: "skills" },
|
|
36324
|
+
{
|
|
36325
|
+
id: "hooks",
|
|
36326
|
+
writesSharedFile: [
|
|
36327
|
+
".augment/settings.json",
|
|
36328
|
+
".claude/settings.json",
|
|
36329
|
+
".codex/config.toml",
|
|
36330
|
+
".config/devin/config.json",
|
|
36331
|
+
".hermes/config.yaml",
|
|
36332
|
+
".kiro/agents/default.json",
|
|
36333
|
+
".qwen/settings.json",
|
|
36334
|
+
".vibe/config.toml"
|
|
36335
|
+
],
|
|
36336
|
+
dependsOn: ["ignore", "mcp"]
|
|
36337
|
+
},
|
|
36338
|
+
{
|
|
36339
|
+
id: "permissions",
|
|
36340
|
+
writesSharedFile: [
|
|
36341
|
+
".amp/settings.json",
|
|
36342
|
+
".augment/settings.json",
|
|
36343
|
+
".claude/settings.json",
|
|
36344
|
+
".codex/config.toml",
|
|
36345
|
+
".config/amp/settings.json",
|
|
36346
|
+
".config/devin/config.json",
|
|
36347
|
+
".config/opencode/opencode.json",
|
|
36348
|
+
".config/zed/settings.json",
|
|
36349
|
+
".devin/config.json",
|
|
36350
|
+
".grok/config.toml",
|
|
36351
|
+
".hermes/config.yaml",
|
|
36352
|
+
".kiro/agents/default.json",
|
|
36353
|
+
".qwen/settings.json",
|
|
36354
|
+
".reasonix/config.toml",
|
|
36355
|
+
".takt/config.yaml",
|
|
36356
|
+
".vibe/config.toml",
|
|
36357
|
+
".zed/settings.json",
|
|
36358
|
+
"opencode.json",
|
|
36359
|
+
"reasonix.toml"
|
|
36360
|
+
],
|
|
36361
|
+
dependsOn: [
|
|
36362
|
+
"ignore",
|
|
36363
|
+
"hooks",
|
|
36364
|
+
"mcp"
|
|
36365
|
+
]
|
|
36366
|
+
},
|
|
36367
|
+
{
|
|
36368
|
+
id: "rules",
|
|
36369
|
+
writesSharedFile: ["kilo.json", "opencode.json"],
|
|
36370
|
+
dependsOn: [
|
|
36371
|
+
"mcp",
|
|
36372
|
+
"skills",
|
|
36373
|
+
"permissions"
|
|
36374
|
+
]
|
|
36375
|
+
}
|
|
36376
|
+
];
|
|
34124
36377
|
/**
|
|
34125
36378
|
* Generate configuration files for AI tools.
|
|
34126
36379
|
* @throws Error if generation fails
|
|
34127
36380
|
*/
|
|
34128
36381
|
async function generate(params) {
|
|
34129
36382
|
const { config, logger } = params;
|
|
34130
|
-
|
|
34131
|
-
|
|
34132
|
-
|
|
34133
|
-
|
|
34134
|
-
|
|
34135
|
-
|
|
34136
|
-
|
|
34137
|
-
|
|
34138
|
-
|
|
34139
|
-
|
|
34140
|
-
|
|
34141
|
-
|
|
34142
|
-
|
|
34143
|
-
|
|
34144
|
-
|
|
34145
|
-
|
|
34146
|
-
|
|
34147
|
-
|
|
34148
|
-
|
|
34149
|
-
|
|
34150
|
-
|
|
34151
|
-
|
|
34152
|
-
|
|
34153
|
-
|
|
34154
|
-
|
|
34155
|
-
|
|
34156
|
-
|
|
34157
|
-
|
|
34158
|
-
|
|
34159
|
-
|
|
34160
|
-
|
|
34161
|
-
|
|
34162
|
-
|
|
34163
|
-
|
|
36383
|
+
let skillsResult;
|
|
36384
|
+
const runners = {
|
|
36385
|
+
ignore: () => generateIgnoreCore({
|
|
36386
|
+
config,
|
|
36387
|
+
logger
|
|
36388
|
+
}),
|
|
36389
|
+
mcp: () => generateMcpCore({
|
|
36390
|
+
config,
|
|
36391
|
+
logger
|
|
36392
|
+
}),
|
|
36393
|
+
commands: () => generateCommandsCore({
|
|
36394
|
+
config,
|
|
36395
|
+
logger
|
|
36396
|
+
}),
|
|
36397
|
+
subagents: () => generateSubagentsCore({
|
|
36398
|
+
config,
|
|
36399
|
+
logger
|
|
36400
|
+
}),
|
|
36401
|
+
skills: async () => {
|
|
36402
|
+
skillsResult = await generateSkillsCore({
|
|
36403
|
+
config,
|
|
36404
|
+
logger
|
|
36405
|
+
});
|
|
36406
|
+
return skillsResult;
|
|
36407
|
+
},
|
|
36408
|
+
hooks: () => generateHooksCore({
|
|
36409
|
+
config,
|
|
36410
|
+
logger
|
|
36411
|
+
}),
|
|
36412
|
+
permissions: () => generatePermissionsCore({
|
|
36413
|
+
config,
|
|
36414
|
+
logger
|
|
36415
|
+
}),
|
|
36416
|
+
rules: () => generateRulesCore({
|
|
36417
|
+
config,
|
|
36418
|
+
logger,
|
|
36419
|
+
skills: skillsResult?.skills
|
|
36420
|
+
})
|
|
36421
|
+
};
|
|
36422
|
+
const orderedSteps = resolveExecutionOrder(GENERATION_STEP_GRAPH.map((meta) => ({
|
|
36423
|
+
...meta,
|
|
36424
|
+
run: runners[meta.id]
|
|
36425
|
+
})));
|
|
36426
|
+
const resultsById = /* @__PURE__ */ new Map();
|
|
36427
|
+
for (const step of orderedSteps) resultsById.set(step.id, await step.run());
|
|
36428
|
+
if (!skillsResult) throw new Error("Skills generation step did not run.");
|
|
36429
|
+
const get = (id) => {
|
|
36430
|
+
const result = resultsById.get(id);
|
|
36431
|
+
if (!result) throw new Error(`Missing generation result for step '${id}'.`);
|
|
36432
|
+
return result;
|
|
36433
|
+
};
|
|
36434
|
+
const hasDiff = orderedSteps.some((step) => get(step.id).hasDiff);
|
|
34164
36435
|
return {
|
|
34165
|
-
rulesCount:
|
|
34166
|
-
rulesPaths:
|
|
34167
|
-
ignoreCount:
|
|
34168
|
-
ignorePaths:
|
|
34169
|
-
mcpCount:
|
|
34170
|
-
mcpPaths:
|
|
34171
|
-
commandsCount:
|
|
34172
|
-
commandsPaths:
|
|
34173
|
-
subagentsCount:
|
|
34174
|
-
subagentsPaths:
|
|
36436
|
+
rulesCount: get("rules").count,
|
|
36437
|
+
rulesPaths: get("rules").paths,
|
|
36438
|
+
ignoreCount: get("ignore").count,
|
|
36439
|
+
ignorePaths: get("ignore").paths,
|
|
36440
|
+
mcpCount: get("mcp").count,
|
|
36441
|
+
mcpPaths: get("mcp").paths,
|
|
36442
|
+
commandsCount: get("commands").count,
|
|
36443
|
+
commandsPaths: get("commands").paths,
|
|
36444
|
+
subagentsCount: get("subagents").count,
|
|
36445
|
+
subagentsPaths: get("subagents").paths,
|
|
34175
36446
|
skillsCount: skillsResult.count,
|
|
34176
36447
|
skillsPaths: skillsResult.paths,
|
|
34177
|
-
hooksCount:
|
|
34178
|
-
hooksPaths:
|
|
34179
|
-
permissionsCount:
|
|
34180
|
-
permissionsPaths:
|
|
36448
|
+
hooksCount: get("hooks").count,
|
|
36449
|
+
hooksPaths: get("hooks").paths,
|
|
36450
|
+
permissionsCount: get("permissions").count,
|
|
36451
|
+
permissionsPaths: get("permissions").paths,
|
|
34181
36452
|
skills: skillsResult.skills,
|
|
34182
36453
|
hasDiff
|
|
34183
36454
|
};
|
|
@@ -34794,4 +37065,4 @@ async function importPermissionsCore(params) {
|
|
|
34794
37065
|
//#endregion
|
|
34795
37066
|
export { CLIError as $, IgnoreProcessor as A, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as At, toolCommandFactories as B, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Bt, PermissionsProcessorToolTargetSchema as C, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Ct, McpProcessorToolTargetSchema as D, RULESYNC_HOOKS_FILE_NAME as Dt, McpProcessor as E, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Et, HooksProcessorToolTargetSchema as F, RULESYNC_PERMISSIONS_FILE_NAME as Ft, CLAUDECODE_SKILLS_DIR_PATH as G, CLAUDECODE_LOCAL_RULE_FILE_NAME as H, formatError as Ht, toolHooksFactories as I, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as It, stringifyFrontmatter as J, RulesyncCommand as K, RulesyncHooks as L, RULESYNC_RELATIVE_DIR_PATH as Lt, toolIgnoreFactories as M, RULESYNC_MCP_RELATIVE_FILE_PATH as Mt, RulesyncIgnore as N, RULESYNC_MCP_SCHEMA_URL as Nt, toolMcpFactories as O, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Ot, HooksProcessor as P, RULESYNC_OVERVIEW_FILE_NAME as Pt, JsonLogger as Q, CommandsProcessor as R, RULESYNC_RULES_RELATIVE_DIR_PATH as Rt, PermissionsProcessor as S, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as St, RulesyncPermissions as T, RULESYNC_CONFIG_SCHEMA_URL as Tt, CLAUDECODE_MEMORIES_DIR_NAME as U, ALL_FEATURES as Ut, CLAUDECODE_DIR as V, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Vt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as W, ALL_FEATURES_WITH_WILDCARD as Wt, findControlCharacter as X, ConfigResolver as Y, ConsoleLogger as Z, toolSkillFactories as _, ALL_TOOL_TARGETS as _t, RulesProcessor as a, fileExists as at, RulesyncSkillFrontmatterSchema as b, MAX_FILE_SIZE as bt, RulesyncRule as c, getHomeDirectory as ct, SubagentsProcessorToolTargetSchema as d, readFileContent as dt, ErrorCodes as et, toolSubagentFactories as f, removeDirectory as ft, SkillsProcessorToolTargetSchema as g, writeFileContent as gt, SkillsProcessor as h, toPosixPath as ht, convertFromTool as i, ensureDir as it, IgnoreProcessorToolTargetSchema as j, RULESYNC_MCP_FILE_NAME as jt, RulesyncMcp as k, RULESYNC_IGNORE_RELATIVE_FILE_PATH as kt, RulesyncRuleFrontmatterSchema as l, isSymlink as lt, RulesyncSubagentFrontmatterSchema as m, removeTempDirectory as mt, checkRulesyncDirExists as n, createTempDirectory as nt, RulesProcessorToolTargetSchema as o, findFilesByGlobs as ot, RulesyncSubagent as p, removeFile as pt, RulesyncCommandFrontmatterSchema as q, generate as r, directoryExists as rt, toolRuleFactories as s, getFileSize as st, importFromTool as t, checkPathTraversal as tt, SubagentsProcessor as u, listDirectoryFiles as ut, getLocalSkillDirNames as v, ALL_TOOL_TARGETS_WITH_WILDCARD as vt, toolPermissionsFactories as w, RULESYNC_CONFIG_RELATIVE_FILE_PATH as wt, SKILL_FILE_NAME as x, RULESYNC_AIIGNORE_FILE_NAME as xt, RulesyncSkill as y, ToolTargetSchema as yt, CommandsProcessorToolTargetSchema as z, RULESYNC_SKILLS_RELATIVE_DIR_PATH as zt };
|
|
34796
37067
|
|
|
34797
|
-
//# sourceMappingURL=import-
|
|
37068
|
+
//# sourceMappingURL=import-DywcWsgJ.js.map
|