rulesync 9.0.2 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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-BhqQwWah.cjs → import-CNtfs80W.cjs} +2697 -477
- package/dist/{import-zmpFGK87.js → import-ylOvQDpE.js} +2688 -468
- package/dist/import-ylOvQDpE.js.map +1 -0
- 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
|
@@ -114,22 +114,22 @@ function formatError(error) {
|
|
|
114
114
|
}
|
|
115
115
|
//#endregion
|
|
116
116
|
//#region src/constants/rulesync-paths.ts
|
|
117
|
-
const { join: join$
|
|
117
|
+
const { join: join$248 } = node_path.posix;
|
|
118
118
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
119
119
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
120
120
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
121
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
122
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
123
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
124
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
125
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
126
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
121
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "rules");
|
|
122
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "commands");
|
|
123
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "subagents");
|
|
124
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
125
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
126
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
127
127
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
128
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
128
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
129
129
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
130
130
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
131
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
132
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
131
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "skills");
|
|
132
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$248(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
|
|
133
133
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
134
134
|
const RULESYNC_MCP_FILE_NAME = "mcp.json";
|
|
135
135
|
const RULESYNC_HOOKS_FILE_NAME = "hooks.json";
|
|
@@ -224,6 +224,7 @@ const mcpProcessorToolTargetTuple = [
|
|
|
224
224
|
"reasonix",
|
|
225
225
|
"roo",
|
|
226
226
|
"rovodev",
|
|
227
|
+
"takt",
|
|
227
228
|
"vibe",
|
|
228
229
|
"warp",
|
|
229
230
|
"devin",
|
|
@@ -231,6 +232,7 @@ const mcpProcessorToolTargetTuple = [
|
|
|
231
232
|
];
|
|
232
233
|
const commandsProcessorToolTargetTuple = [
|
|
233
234
|
"agentsmd",
|
|
235
|
+
"antigravity-cli",
|
|
234
236
|
"antigravity-ide",
|
|
235
237
|
"augmentcode",
|
|
236
238
|
"claudecode",
|
|
@@ -250,7 +252,9 @@ const commandsProcessorToolTargetTuple = [
|
|
|
250
252
|
"opencode",
|
|
251
253
|
"pi",
|
|
252
254
|
"qwencode",
|
|
255
|
+
"reasonix",
|
|
253
256
|
"roo",
|
|
257
|
+
"rovodev",
|
|
254
258
|
"takt",
|
|
255
259
|
"devin"
|
|
256
260
|
];
|
|
@@ -335,11 +339,13 @@ const hooksProcessorToolTargetTuple = [
|
|
|
335
339
|
"deepagents",
|
|
336
340
|
"kiro",
|
|
337
341
|
"kiro-cli",
|
|
342
|
+
"kiro-ide",
|
|
338
343
|
"devin",
|
|
339
344
|
"augmentcode",
|
|
340
345
|
"junie",
|
|
341
346
|
"vibe",
|
|
342
|
-
"qwencode"
|
|
347
|
+
"qwencode",
|
|
348
|
+
"reasonix"
|
|
343
349
|
];
|
|
344
350
|
const permissionsProcessorToolTargetTuple = [
|
|
345
351
|
"amp",
|
|
@@ -350,16 +356,19 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
350
356
|
"cline",
|
|
351
357
|
"codexcli",
|
|
352
358
|
"cursor",
|
|
359
|
+
"devin",
|
|
353
360
|
"factorydroid",
|
|
354
361
|
"goose",
|
|
355
362
|
"grokcli",
|
|
356
363
|
"hermesagent",
|
|
364
|
+
"junie",
|
|
357
365
|
"kilo",
|
|
358
366
|
"kiro",
|
|
359
367
|
"kiro-cli",
|
|
360
368
|
"kiro-ide",
|
|
361
369
|
"opencode",
|
|
362
370
|
"qwencode",
|
|
371
|
+
"reasonix",
|
|
363
372
|
"rovodev",
|
|
364
373
|
"takt",
|
|
365
374
|
"vibe",
|
|
@@ -1399,7 +1408,7 @@ function extractConfigFileTargets(targets) {
|
|
|
1399
1408
|
* Type guard to check if a value is a plain object (Record<string, unknown>).
|
|
1400
1409
|
* This excludes arrays and null values.
|
|
1401
1410
|
*/
|
|
1402
|
-
function isRecord
|
|
1411
|
+
function isRecord(value) {
|
|
1403
1412
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1404
1413
|
}
|
|
1405
1414
|
/**
|
|
@@ -1414,7 +1423,7 @@ function isRecord$1(value) {
|
|
|
1414
1423
|
* malicious accessor descriptors.
|
|
1415
1424
|
*/
|
|
1416
1425
|
function isPlainObject(value) {
|
|
1417
|
-
if (!isRecord
|
|
1426
|
+
if (!isRecord(value)) return false;
|
|
1418
1427
|
const proto = Object.getPrototypeOf(value);
|
|
1419
1428
|
return proto === null || proto === Object.prototype;
|
|
1420
1429
|
}
|
|
@@ -1765,6 +1774,17 @@ var ToolCommand = class extends AiFile {
|
|
|
1765
1774
|
throw new Error("Please implement this method in the subclass.");
|
|
1766
1775
|
}
|
|
1767
1776
|
/**
|
|
1777
|
+
* Optional hook for tools whose commands are not purely one-file-per-command
|
|
1778
|
+
* (e.g. Rovo Dev's `prompts.yml` manifest that indexes every saved-prompt
|
|
1779
|
+
* content file). Given the full set of ToolCommand instances just generated
|
|
1780
|
+
* for this target, returns any additional shared/aggregate files that must
|
|
1781
|
+
* be written alongside them. Most tools don't need this and inherit the
|
|
1782
|
+
* empty default (mirrors `ToolHooks.getAuxiliaryFiles`).
|
|
1783
|
+
*/
|
|
1784
|
+
static async getAuxiliaryFiles(_params) {
|
|
1785
|
+
return [];
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1768
1788
|
* Convert a RulesyncCommand to the tool-specific command format.
|
|
1769
1789
|
*
|
|
1770
1790
|
* This method should:
|
|
@@ -1923,16 +1943,25 @@ var AgentsmdCommand = class AgentsmdCommand extends SimulatedCommand {
|
|
|
1923
1943
|
}
|
|
1924
1944
|
};
|
|
1925
1945
|
//#endregion
|
|
1926
|
-
//#region src/constants/antigravity-
|
|
1927
|
-
const
|
|
1928
|
-
const
|
|
1929
|
-
const
|
|
1930
|
-
const
|
|
1931
|
-
const
|
|
1932
|
-
const
|
|
1933
|
-
const
|
|
1934
|
-
const
|
|
1935
|
-
const
|
|
1946
|
+
//#region src/constants/antigravity-paths.ts
|
|
1947
|
+
const ANTIGRAVITY_DIR = ".agents";
|
|
1948
|
+
const ANTIGRAVITY_SKILLS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_DIR, "skills");
|
|
1949
|
+
const ANTIGRAVITY_WORKFLOWS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_DIR, "workflows");
|
|
1950
|
+
const ANTIGRAVITY_MCP_FILE_NAME = "mcp_config.json";
|
|
1951
|
+
const ANTIGRAVITY_HOOKS_FILE_NAME = "hooks.json";
|
|
1952
|
+
const ANTIGRAVITY_IGNORE_FILE_NAME = ".geminiignore";
|
|
1953
|
+
const ANTIGRAVITY_GEMINI_DIR = ".gemini";
|
|
1954
|
+
const ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR = "antigravity-cli";
|
|
1955
|
+
const ANTIGRAVITY_CLI_PERMISSIONS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR);
|
|
1956
|
+
const ANTIGRAVITY_CLI_PERMISSIONS_FILE_NAME = "settings.json";
|
|
1957
|
+
const ANTIGRAVITY_CLI_GLOBAL_WORKFLOWS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR, "global_workflows");
|
|
1958
|
+
const ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR = "config";
|
|
1959
|
+
const ANTIGRAVITY_GLOBAL_CONFIG_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR);
|
|
1960
|
+
//#endregion
|
|
1961
|
+
//#region src/constants/antigravity-cli-paths.ts
|
|
1962
|
+
const ANTIGRAVITY_AGENTS_DIR = ANTIGRAVITY_DIR;
|
|
1963
|
+
const ANTIGRAVITY_RULE_FILE_NAME = "AGENTS.md";
|
|
1964
|
+
const ANTIGRAVITY_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
|
|
1936
1965
|
//#endregion
|
|
1937
1966
|
//#region src/features/commands/antigravity-command.ts
|
|
1938
1967
|
const AntigravityWorkflowFrontmatterSchema = zod_mini.z.looseObject({
|
|
@@ -2106,7 +2135,7 @@ var AntigravitySharedCommand = class extends ToolCommand {
|
|
|
2106
2135
|
}
|
|
2107
2136
|
static extractAntigravityConfig(rulesyncCommand) {
|
|
2108
2137
|
const antigravity = rulesyncCommand.getFrontmatter().antigravity;
|
|
2109
|
-
return isRecord
|
|
2138
|
+
return isRecord(antigravity) ? antigravity : void 0;
|
|
2110
2139
|
}
|
|
2111
2140
|
static resolveTrigger(rulesyncCommand, antigravityConfig) {
|
|
2112
2141
|
const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
|
|
@@ -2188,6 +2217,45 @@ var AntigravitySharedCommand = class extends ToolCommand {
|
|
|
2188
2217
|
}
|
|
2189
2218
|
};
|
|
2190
2219
|
//#endregion
|
|
2220
|
+
//#region src/features/commands/antigravity-cli-command.ts
|
|
2221
|
+
/**
|
|
2222
|
+
* Command (workflow) generator for the Google Antigravity CLI (`agy`, Antigravity 2.0).
|
|
2223
|
+
*
|
|
2224
|
+
* Shares all body and frontmatter handling with {@link AntigravitySharedCommand};
|
|
2225
|
+
* the CLI reads project workflows from the same `.agents/workflows/` directory as
|
|
2226
|
+
* the IDE (the shared Antigravity 2.0 harness), but keeps its own global
|
|
2227
|
+
* workflows tree at `~/.gemini/antigravity-cli/global_workflows/` (mirroring the
|
|
2228
|
+
* CLI's global skills tree). It answers to the `antigravity-cli` target.
|
|
2229
|
+
*/
|
|
2230
|
+
var AntigravityCliCommand = class extends AntigravitySharedCommand {
|
|
2231
|
+
static getProjectRelativeDirPath() {
|
|
2232
|
+
return ANTIGRAVITY_WORKFLOWS_DIR_PATH;
|
|
2233
|
+
}
|
|
2234
|
+
static getGlobalRelativeDirPath() {
|
|
2235
|
+
return ANTIGRAVITY_CLI_GLOBAL_WORKFLOWS_DIR_PATH;
|
|
2236
|
+
}
|
|
2237
|
+
getToolTargetName() {
|
|
2238
|
+
return "antigravity-cli";
|
|
2239
|
+
}
|
|
2240
|
+
static isTargetedByRulesyncCommand(rulesyncCommand) {
|
|
2241
|
+
return this.isTargetedByRulesyncCommandDefault({
|
|
2242
|
+
rulesyncCommand,
|
|
2243
|
+
toolTarget: "antigravity-cli"
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
};
|
|
2247
|
+
//#endregion
|
|
2248
|
+
//#region src/constants/antigravity-ide-paths.ts
|
|
2249
|
+
const ANTIGRAVITY_IDE_AGENTS_DIR = ".agents";
|
|
2250
|
+
const ANTIGRAVITY_IDE_COMMANDS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_IDE_AGENTS_DIR, "workflows");
|
|
2251
|
+
const ANTIGRAVITY_IDE_RULE_FILE_NAME = "AGENTS.md";
|
|
2252
|
+
const ANTIGRAVITY_IDE_GEMINI_DIR = ".gemini";
|
|
2253
|
+
const ANTIGRAVITY_IDE_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
|
|
2254
|
+
const ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR = "config";
|
|
2255
|
+
const ANTIGRAVITY_IDE_GLOBAL_WORKFLOWS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_IDE_GEMINI_DIR, "antigravity", "global_workflows");
|
|
2256
|
+
const ANTIGRAVITY_IDE_PERMISSIONS_DIR = ".antigravity";
|
|
2257
|
+
const ANTIGRAVITY_IDE_PERMISSIONS_FILE_NAME = "settings.json";
|
|
2258
|
+
//#endregion
|
|
2191
2259
|
//#region src/features/commands/antigravity-ide-command.ts
|
|
2192
2260
|
/**
|
|
2193
2261
|
* Command (workflow) generator for the Google Antigravity IDE (Antigravity 2.0).
|
|
@@ -2572,6 +2640,16 @@ const CodexcliCommandFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
2572
2640
|
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
2573
2641
|
"argument-hint": zod_mini.z.optional(zod_mini.z.string())
|
|
2574
2642
|
});
|
|
2643
|
+
/**
|
|
2644
|
+
* Generates Codex CLI's global-only custom prompts under `~/.codex/prompts/*.md`.
|
|
2645
|
+
*
|
|
2646
|
+
* Note: upstream Codex docs now state "Custom prompts are deprecated. Use skills for
|
|
2647
|
+
* reusable instructions" (https://developers.openai.com/codex/custom-prompts). No removal
|
|
2648
|
+
* date has been announced and custom prompts remain functional, so this class's
|
|
2649
|
+
* generation behavior is unchanged. Prefer rulesync's `codexcli` skills support
|
|
2650
|
+
* (see `src/features/skills/codexcli-skill.ts`) for new reusable instructions going
|
|
2651
|
+
* forward; this class is kept for users who still rely on custom prompts.
|
|
2652
|
+
*/
|
|
2575
2653
|
var CodexcliCommand = class CodexcliCommand extends ToolCommand {
|
|
2576
2654
|
frontmatter;
|
|
2577
2655
|
body;
|
|
@@ -2695,7 +2773,7 @@ const COPILOTCLI_PROJECT_MCP_FILE_NAME = "mcp.json";
|
|
|
2695
2773
|
const COPILOTCLI_AGENTS_DIR_PATH = (0, node_path.join)(COPILOT_DIR, "agents");
|
|
2696
2774
|
const COPILOTCLI_HOOKS_DIR_PATH = (0, node_path.join)(COPILOT_DIR, "hooks");
|
|
2697
2775
|
const COPILOTCLI_HOOKS_FILE_NAME = "copilotcli-hooks.json";
|
|
2698
|
-
const
|
|
2776
|
+
const COPILOT_SKILLS_GLOBAL_DIR_PATH = (0, node_path.join)(COPILOT_DIR, "skills");
|
|
2699
2777
|
//#endregion
|
|
2700
2778
|
//#region src/features/commands/copilot-command.ts
|
|
2701
2779
|
const CopilotCommandFrontmatterSchema = zod_mini.z.looseObject({
|
|
@@ -2945,20 +3023,17 @@ var CursorCommand = class CursorCommand extends ToolCommand {
|
|
|
2945
3023
|
//#endregion
|
|
2946
3024
|
//#region src/constants/devin-paths.ts
|
|
2947
3025
|
const DEVIN_DIR = ".devin";
|
|
2948
|
-
const
|
|
2949
|
-
const CODEIUM_DIR = ".codeium";
|
|
2950
|
-
const WINDSURF_SUBDIR = "windsurf";
|
|
2951
|
-
const CODEIUM_WINDSURF_DIR = (0, node_path.join)(CODEIUM_DIR, WINDSURF_SUBDIR);
|
|
2952
|
-
const WINDSURF_MEMORIES_SUBDIR = (0, node_path.join)(WINDSURF_SUBDIR, "memories");
|
|
3026
|
+
const CODEIUM_WINDSURF_DIR = (0, node_path.join)(".codeium", "windsurf");
|
|
2953
3027
|
const DEVIN_WORKFLOWS_DIR_PATH = (0, node_path.join)(DEVIN_DIR, "workflows");
|
|
2954
3028
|
const DEVIN_SKILLS_DIR_PATH = (0, node_path.join)(DEVIN_DIR, "skills");
|
|
2955
3029
|
const DEVIN_AGENTS_DIR_PATH = (0, node_path.join)(DEVIN_DIR, "agents");
|
|
2956
|
-
const
|
|
3030
|
+
const DEVIN_GLOBAL_CONFIG_DIR_PATH = (0, node_path.join)(".config", "devin");
|
|
3031
|
+
const DEVIN_GLOBAL_AGENTS_DIR_PATH = (0, node_path.join)(DEVIN_GLOBAL_CONFIG_DIR_PATH, "agents");
|
|
2957
3032
|
const CODEIUM_WINDSURF_GLOBAL_WORKFLOWS_DIR_PATH = (0, node_path.join)(CODEIUM_WINDSURF_DIR, "global_workflows");
|
|
2958
3033
|
const CODEIUM_WINDSURF_SKILLS_DIR_PATH = (0, node_path.join)(CODEIUM_WINDSURF_DIR, "skills");
|
|
2959
|
-
const
|
|
2960
|
-
const
|
|
2961
|
-
const
|
|
3034
|
+
const DEVIN_CONFIG_FILE_NAME = "config.json";
|
|
3035
|
+
const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
|
|
3036
|
+
const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
|
|
2962
3037
|
const DEVIN_IGNORE_FILE_NAME = ".devinignore";
|
|
2963
3038
|
const DEVIN_LEGACY_IGNORE_FILE_NAME = ".codeiumignore";
|
|
2964
3039
|
//#endregion
|
|
@@ -3207,6 +3282,8 @@ const GOOSE_MCP_FILE_NAME = "config.yaml";
|
|
|
3207
3282
|
const GOOSE_PERMISSIONS_FILE_NAME = "permission.yaml";
|
|
3208
3283
|
const GOOSE_HOOKS_DIR_PATH = (0, node_path.join)(".agents", "plugins", "rulesync", "hooks");
|
|
3209
3284
|
const GOOSE_HOOKS_FILE_NAME = "hooks.json";
|
|
3285
|
+
const GOOSE_PLUGIN_MCP_DIR_PATH = (0, node_path.join)(".agents", "plugins", "rulesync");
|
|
3286
|
+
const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";
|
|
3210
3287
|
const GOOSE_SKILLS_DIR_PATH = (0, node_path.join)(GOOSE_DIR, "skills");
|
|
3211
3288
|
const GOOSE_RECIPES_DIR_PATH = (0, node_path.join)(GOOSE_DIR, "recipes");
|
|
3212
3289
|
const GOOSE_GLOBAL_RECIPES_DIR_PATH = (0, node_path.join)(GOOSE_GLOBAL_DIR, "recipes");
|
|
@@ -3465,6 +3542,7 @@ const JUNIE_ALT_AGENTS_DIR_PATH = ".agents";
|
|
|
3465
3542
|
const JUNIE_MCP_DIR_PATH = (0, node_path.join)(JUNIE_DIR, "mcp");
|
|
3466
3543
|
const JUNIE_MCP_FILE_NAME = "mcp.json";
|
|
3467
3544
|
const JUNIE_HOOKS_FILE_NAME = "config.json";
|
|
3545
|
+
const JUNIE_PERMISSIONS_FILE_NAME = "allowlist.json";
|
|
3468
3546
|
const JUNIE_IGNORE_FILE_NAME = ".aiignore";
|
|
3469
3547
|
const JUNIE_RULE_FILE_NAME = "AGENTS.md";
|
|
3470
3548
|
const JUNIE_LEGACY_RULE_FILE_NAME = "guidelines.md";
|
|
@@ -3717,6 +3795,15 @@ const KIRO_SKILLS_DIR_PATH = (0, node_path.join)(KIRO_DIR, "skills");
|
|
|
3717
3795
|
const KIRO_SETTINGS_DIR_PATH = (0, node_path.join)(KIRO_DIR, "settings");
|
|
3718
3796
|
const KIRO_AGENTS_DIR_PATH = (0, node_path.join)(KIRO_DIR, "agents");
|
|
3719
3797
|
const KIRO_HOOKS_FILE_NAME = "default.json";
|
|
3798
|
+
/**
|
|
3799
|
+
* Kiro IDE 1.0 stores hooks as structured JSON files in `.kiro/hooks/`
|
|
3800
|
+
* (workspace) and `~/.kiro/hooks/` (user). A single file may declare multiple
|
|
3801
|
+
* hooks in its `hooks` array, so rulesync emits all generated hooks into one
|
|
3802
|
+
* `rulesync.json` file per scope.
|
|
3803
|
+
* @see https://kiro.dev/docs/hooks/
|
|
3804
|
+
*/
|
|
3805
|
+
const KIRO_IDE_HOOKS_DIR_PATH = (0, node_path.join)(KIRO_DIR, "hooks");
|
|
3806
|
+
const KIRO_IDE_HOOKS_FILE_NAME = "rulesync.json";
|
|
3720
3807
|
const KIRO_MCP_FILE_NAME = "mcp.json";
|
|
3721
3808
|
const KIRO_IGNORE_FILE_NAME = ".kiroignore";
|
|
3722
3809
|
//#endregion
|
|
@@ -4296,6 +4383,135 @@ var QwencodeCommand = class QwencodeCommand extends ToolCommand {
|
|
|
4296
4383
|
}
|
|
4297
4384
|
};
|
|
4298
4385
|
//#endregion
|
|
4386
|
+
//#region src/constants/reasonix-paths.ts
|
|
4387
|
+
const REASONIX_PROJECT_MCP_FILE_NAME = "reasonix.toml";
|
|
4388
|
+
const REASONIX_GLOBAL_DIR = ".reasonix";
|
|
4389
|
+
const REASONIX_GLOBAL_MCP_FILE_NAME = "config.toml";
|
|
4390
|
+
const REASONIX_PROJECT_PERMISSIONS_FILE_NAME = REASONIX_PROJECT_MCP_FILE_NAME;
|
|
4391
|
+
const REASONIX_GLOBAL_PERMISSIONS_FILE_NAME = REASONIX_GLOBAL_MCP_FILE_NAME;
|
|
4392
|
+
const REASONIX_DIR = REASONIX_GLOBAL_DIR;
|
|
4393
|
+
const REASONIX_SETTINGS_FILE_NAME = "settings.json";
|
|
4394
|
+
const REASONIX_COMMANDS_DIR_PATH = (0, node_path.join)(REASONIX_DIR, "commands");
|
|
4395
|
+
//#endregion
|
|
4396
|
+
//#region src/features/commands/reasonix-command.ts
|
|
4397
|
+
/**
|
|
4398
|
+
* Reasonix custom slash commands are Markdown files under `.reasonix/commands/`
|
|
4399
|
+
* (project) / `~/.reasonix/commands/` (global) — directly analogous to Claude
|
|
4400
|
+
* Code's `.claude/commands/` (Reasonix explicitly copies Claude Code's
|
|
4401
|
+
* conventions). Frontmatter supports `description` and `argument-hint`; the
|
|
4402
|
+
* body uses the same `$ARGUMENTS` / `$1`…`$N` placeholder syntax rulesync's
|
|
4403
|
+
* universal command-body syntax already targets.
|
|
4404
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/GUIDE.md
|
|
4405
|
+
*/
|
|
4406
|
+
const ReasonixCommandFrontmatterSchema = zod_mini.z.looseObject({
|
|
4407
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
4408
|
+
"argument-hint": zod_mini.z.optional(zod_mini.z.string())
|
|
4409
|
+
});
|
|
4410
|
+
var ReasonixCommand = class ReasonixCommand extends ToolCommand {
|
|
4411
|
+
frontmatter;
|
|
4412
|
+
body;
|
|
4413
|
+
constructor({ frontmatter, body, ...rest }) {
|
|
4414
|
+
if (rest.validate) {
|
|
4415
|
+
const result = ReasonixCommandFrontmatterSchema.safeParse(frontmatter);
|
|
4416
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
4417
|
+
}
|
|
4418
|
+
super({
|
|
4419
|
+
...rest,
|
|
4420
|
+
fileContent: stringifyFrontmatter(body, frontmatter)
|
|
4421
|
+
});
|
|
4422
|
+
this.frontmatter = frontmatter;
|
|
4423
|
+
this.body = body;
|
|
4424
|
+
}
|
|
4425
|
+
static getSettablePaths(_options = {}) {
|
|
4426
|
+
return { relativeDirPath: REASONIX_COMMANDS_DIR_PATH };
|
|
4427
|
+
}
|
|
4428
|
+
getBody() {
|
|
4429
|
+
return this.body;
|
|
4430
|
+
}
|
|
4431
|
+
getFrontmatter() {
|
|
4432
|
+
return this.frontmatter;
|
|
4433
|
+
}
|
|
4434
|
+
toRulesyncCommand() {
|
|
4435
|
+
const { description, ...restFields } = this.frontmatter;
|
|
4436
|
+
const rulesyncFrontmatter = {
|
|
4437
|
+
targets: ["*"],
|
|
4438
|
+
description,
|
|
4439
|
+
...Object.keys(restFields).length > 0 && { reasonix: restFields }
|
|
4440
|
+
};
|
|
4441
|
+
const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter);
|
|
4442
|
+
return new RulesyncCommand({
|
|
4443
|
+
outputRoot: ".",
|
|
4444
|
+
frontmatter: rulesyncFrontmatter,
|
|
4445
|
+
body: this.body,
|
|
4446
|
+
relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
|
|
4447
|
+
relativeFilePath: this.relativeFilePath,
|
|
4448
|
+
fileContent,
|
|
4449
|
+
validate: true
|
|
4450
|
+
});
|
|
4451
|
+
}
|
|
4452
|
+
static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
|
|
4453
|
+
const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
|
|
4454
|
+
const reasonixFields = rulesyncFrontmatter.reasonix ?? {};
|
|
4455
|
+
return new ReasonixCommand({
|
|
4456
|
+
outputRoot,
|
|
4457
|
+
frontmatter: {
|
|
4458
|
+
description: rulesyncFrontmatter.description,
|
|
4459
|
+
...reasonixFields
|
|
4460
|
+
},
|
|
4461
|
+
body: rulesyncCommand.getBody(),
|
|
4462
|
+
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
4463
|
+
relativeFilePath: rulesyncCommand.getRelativeFilePath(),
|
|
4464
|
+
validate
|
|
4465
|
+
});
|
|
4466
|
+
}
|
|
4467
|
+
validate() {
|
|
4468
|
+
if (!this.frontmatter) return {
|
|
4469
|
+
success: true,
|
|
4470
|
+
error: null
|
|
4471
|
+
};
|
|
4472
|
+
const result = ReasonixCommandFrontmatterSchema.safeParse(this.frontmatter);
|
|
4473
|
+
if (result.success) return {
|
|
4474
|
+
success: true,
|
|
4475
|
+
error: null
|
|
4476
|
+
};
|
|
4477
|
+
else return {
|
|
4478
|
+
success: false,
|
|
4479
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
4480
|
+
};
|
|
4481
|
+
}
|
|
4482
|
+
static isTargetedByRulesyncCommand(rulesyncCommand) {
|
|
4483
|
+
return this.isTargetedByRulesyncCommandDefault({
|
|
4484
|
+
rulesyncCommand,
|
|
4485
|
+
toolTarget: "reasonix"
|
|
4486
|
+
});
|
|
4487
|
+
}
|
|
4488
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
4489
|
+
const paths = this.getSettablePaths({ global });
|
|
4490
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
4491
|
+
const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
4492
|
+
const result = ReasonixCommandFrontmatterSchema.safeParse(frontmatter);
|
|
4493
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
4494
|
+
return new ReasonixCommand({
|
|
4495
|
+
outputRoot,
|
|
4496
|
+
relativeDirPath: paths.relativeDirPath,
|
|
4497
|
+
relativeFilePath,
|
|
4498
|
+
frontmatter: result.data,
|
|
4499
|
+
body: content.trim(),
|
|
4500
|
+
validate
|
|
4501
|
+
});
|
|
4502
|
+
}
|
|
4503
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
4504
|
+
return new ReasonixCommand({
|
|
4505
|
+
outputRoot,
|
|
4506
|
+
relativeDirPath,
|
|
4507
|
+
relativeFilePath,
|
|
4508
|
+
frontmatter: { description: "" },
|
|
4509
|
+
body: "",
|
|
4510
|
+
validate: false
|
|
4511
|
+
});
|
|
4512
|
+
}
|
|
4513
|
+
};
|
|
4514
|
+
//#endregion
|
|
4299
4515
|
//#region src/constants/roo-paths.ts
|
|
4300
4516
|
const ROO_DIR = ".roo";
|
|
4301
4517
|
const ROO_COMMANDS_DIR_PATH = (0, node_path.join)(ROO_DIR, "commands");
|
|
@@ -4427,6 +4643,217 @@ var RooCommand = class RooCommand extends ToolCommand {
|
|
|
4427
4643
|
}
|
|
4428
4644
|
};
|
|
4429
4645
|
//#endregion
|
|
4646
|
+
//#region src/constants/rovodev-paths.ts
|
|
4647
|
+
const ROVODEV_DIR = ".rovodev";
|
|
4648
|
+
const ROVODEV_SKILLS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "skills");
|
|
4649
|
+
const ROVODEV_SUBAGENTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "subagents");
|
|
4650
|
+
const ROVODEV_MODULAR_RULES_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, ".rulesync", "modular-rules");
|
|
4651
|
+
const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
|
|
4652
|
+
const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
|
|
4653
|
+
const ROVODEV_MCP_FILE_NAME = "mcp.json";
|
|
4654
|
+
const ROVODEV_CONFIG_FILE_NAME = "config.yml";
|
|
4655
|
+
const ROVODEV_AGENTS_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
|
|
4656
|
+
const ROVODEV_PROMPTS_FILE_NAME = "prompts.yml";
|
|
4657
|
+
const ROVODEV_PROMPTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "prompts");
|
|
4658
|
+
//#endregion
|
|
4659
|
+
//#region src/types/tool-file.ts
|
|
4660
|
+
var ToolFile = class extends AiFile {};
|
|
4661
|
+
//#endregion
|
|
4662
|
+
//#region src/features/commands/rovodev-command.ts
|
|
4663
|
+
/**
|
|
4664
|
+
* Rovo Dev CLI "saved prompts": a file-based custom-command surface made of a
|
|
4665
|
+
* `prompts.yml` manifest (`{ name, description, content_file }` entries) plus
|
|
4666
|
+
* per-prompt Markdown content files, discovered in repo-root `.rovodev/`, cwd
|
|
4667
|
+
* `.rovodev/`, and global `~/.rovodev/`, and invoked via `/prompts [title] [extra]`.
|
|
4668
|
+
*
|
|
4669
|
+
* This class represents a single prompt's **content file** — pure Markdown,
|
|
4670
|
+
* no frontmatter — written to `.rovodev/prompts/<name>.md` (project) or
|
|
4671
|
+
* `~/.rovodev/prompts/<name>.md` (global). The `name`/`description` are kept
|
|
4672
|
+
* on the instance (not serialized into the content file itself) so that
|
|
4673
|
+
* {@link RovodevCommand.getAuxiliaryFiles} can build the sibling
|
|
4674
|
+
* `prompts.yml` manifest that indexes every generated prompt.
|
|
4675
|
+
*
|
|
4676
|
+
* @see https://support.atlassian.com/rovo/docs/save-and-reuse-a-prompt-in-rovo-dev-cli/
|
|
4677
|
+
* @see https://support.atlassian.com/rovo/docs/rovo-dev-cli-commands/
|
|
4678
|
+
*/
|
|
4679
|
+
var RovodevCommand = class RovodevCommand extends ToolCommand {
|
|
4680
|
+
name;
|
|
4681
|
+
description;
|
|
4682
|
+
body;
|
|
4683
|
+
constructor({ name, description, body, ...rest }) {
|
|
4684
|
+
super({
|
|
4685
|
+
...rest,
|
|
4686
|
+
fileContent: body
|
|
4687
|
+
});
|
|
4688
|
+
this.name = name;
|
|
4689
|
+
this.description = description;
|
|
4690
|
+
this.body = body;
|
|
4691
|
+
if (rest.validate) {
|
|
4692
|
+
const result = this.validate();
|
|
4693
|
+
if (!result.success) throw result.error;
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
static getSettablePaths(_options = {}) {
|
|
4697
|
+
return { relativeDirPath: ROVODEV_PROMPTS_DIR_PATH };
|
|
4698
|
+
}
|
|
4699
|
+
getName() {
|
|
4700
|
+
return this.name;
|
|
4701
|
+
}
|
|
4702
|
+
getDescription() {
|
|
4703
|
+
return this.description;
|
|
4704
|
+
}
|
|
4705
|
+
getBody() {
|
|
4706
|
+
return this.body;
|
|
4707
|
+
}
|
|
4708
|
+
validate() {
|
|
4709
|
+
if (!this.name) return {
|
|
4710
|
+
success: false,
|
|
4711
|
+
error: /* @__PURE__ */ new Error(`${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: Rovo Dev saved-prompt name must not be empty`)
|
|
4712
|
+
};
|
|
4713
|
+
return {
|
|
4714
|
+
success: true,
|
|
4715
|
+
error: null
|
|
4716
|
+
};
|
|
4717
|
+
}
|
|
4718
|
+
toRulesyncCommand() {
|
|
4719
|
+
const rulesyncFrontmatter = {
|
|
4720
|
+
targets: ["*"],
|
|
4721
|
+
description: this.description
|
|
4722
|
+
};
|
|
4723
|
+
const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter);
|
|
4724
|
+
return new RulesyncCommand({
|
|
4725
|
+
outputRoot: ".",
|
|
4726
|
+
frontmatter: rulesyncFrontmatter,
|
|
4727
|
+
body: this.body,
|
|
4728
|
+
relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
|
|
4729
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
4730
|
+
fileContent,
|
|
4731
|
+
validate: true
|
|
4732
|
+
});
|
|
4733
|
+
}
|
|
4734
|
+
static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
|
|
4735
|
+
const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
|
|
4736
|
+
const relativeFilePath = rulesyncCommand.getRelativeFilePath();
|
|
4737
|
+
const name = (0, node_path.basename)(relativeFilePath, ".md");
|
|
4738
|
+
const paths = this.getSettablePaths({ global });
|
|
4739
|
+
return new RovodevCommand({
|
|
4740
|
+
outputRoot,
|
|
4741
|
+
name,
|
|
4742
|
+
description: rulesyncFrontmatter.description ?? "",
|
|
4743
|
+
body: rulesyncCommand.getBody(),
|
|
4744
|
+
relativeDirPath: paths.relativeDirPath,
|
|
4745
|
+
relativeFilePath,
|
|
4746
|
+
validate,
|
|
4747
|
+
global
|
|
4748
|
+
});
|
|
4749
|
+
}
|
|
4750
|
+
static isTargetedByRulesyncCommand(rulesyncCommand) {
|
|
4751
|
+
return this.isTargetedByRulesyncCommandDefault({
|
|
4752
|
+
rulesyncCommand,
|
|
4753
|
+
toolTarget: "rovodev"
|
|
4754
|
+
});
|
|
4755
|
+
}
|
|
4756
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
4757
|
+
const paths = this.getSettablePaths({ global });
|
|
4758
|
+
const body = (await readFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath))).trim();
|
|
4759
|
+
const name = (0, node_path.basename)(relativeFilePath, ".md");
|
|
4760
|
+
return new RovodevCommand({
|
|
4761
|
+
outputRoot,
|
|
4762
|
+
name,
|
|
4763
|
+
description: await lookupPromptDescription({
|
|
4764
|
+
outputRoot,
|
|
4765
|
+
relativeFilePath,
|
|
4766
|
+
name
|
|
4767
|
+
}),
|
|
4768
|
+
body,
|
|
4769
|
+
relativeDirPath: paths.relativeDirPath,
|
|
4770
|
+
relativeFilePath,
|
|
4771
|
+
validate,
|
|
4772
|
+
global
|
|
4773
|
+
});
|
|
4774
|
+
}
|
|
4775
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
4776
|
+
return new RovodevCommand({
|
|
4777
|
+
outputRoot,
|
|
4778
|
+
name: (0, node_path.basename)(relativeFilePath, ".md"),
|
|
4779
|
+
description: "",
|
|
4780
|
+
body: "",
|
|
4781
|
+
relativeDirPath,
|
|
4782
|
+
relativeFilePath,
|
|
4783
|
+
validate: false,
|
|
4784
|
+
global
|
|
4785
|
+
});
|
|
4786
|
+
}
|
|
4787
|
+
/**
|
|
4788
|
+
* Rebuilds `.rovodev/prompts.yml` (project) / `~/.rovodev/prompts.yml`
|
|
4789
|
+
* (global) from every `RovodevCommand` generated in this pass. The `prompts`
|
|
4790
|
+
* array is fully replaced with the current set of rulesync-managed prompts
|
|
4791
|
+
* (mirrors `RovodevMcp` fully replacing `mcpServers`); any other top-level
|
|
4792
|
+
* key in an existing manifest is preserved.
|
|
4793
|
+
*/
|
|
4794
|
+
static async getAuxiliaryFiles({ toolCommands, outputRoot = process.cwd(), global = false }) {
|
|
4795
|
+
const rovodevCommands = toolCommands.filter((command) => command instanceof RovodevCommand);
|
|
4796
|
+
if (rovodevCommands.length === 0) return [];
|
|
4797
|
+
const existingContent = await readFileContentOrNull((0, node_path.join)(outputRoot, ROVODEV_DIR, ROVODEV_PROMPTS_FILE_NAME));
|
|
4798
|
+
let existing = {};
|
|
4799
|
+
if (existingContent) try {
|
|
4800
|
+
const parsed = (0, js_yaml.load)(existingContent);
|
|
4801
|
+
if (isPlainObject(parsed)) existing = parsed;
|
|
4802
|
+
} catch {}
|
|
4803
|
+
const prompts = rovodevCommands.map((command) => ({
|
|
4804
|
+
name: command.getName(),
|
|
4805
|
+
description: command.getDescription(),
|
|
4806
|
+
content_file: toPosixPath((0, node_path.join)("prompts", command.getRelativeFilePath()))
|
|
4807
|
+
})).toSorted((a, b) => a.name.localeCompare(b.name));
|
|
4808
|
+
return [new RovodevPromptsManifest({
|
|
4809
|
+
outputRoot,
|
|
4810
|
+
relativeDirPath: ROVODEV_DIR,
|
|
4811
|
+
relativeFilePath: ROVODEV_PROMPTS_FILE_NAME,
|
|
4812
|
+
fileContent: (0, js_yaml.dump)({
|
|
4813
|
+
...existing,
|
|
4814
|
+
prompts
|
|
4815
|
+
}),
|
|
4816
|
+
global
|
|
4817
|
+
})];
|
|
4818
|
+
}
|
|
4819
|
+
};
|
|
4820
|
+
/**
|
|
4821
|
+
* The `prompts` entry shape read back from `content_file`, matched against
|
|
4822
|
+
* either the resolved path (relative to `prompts.yml`, i.e. the `.rovodev/`
|
|
4823
|
+
* directory) or the entry `name`, to recover the `description` that isn't
|
|
4824
|
+
* stored in the content file itself.
|
|
4825
|
+
*/
|
|
4826
|
+
async function lookupPromptDescription({ outputRoot, relativeFilePath, name }) {
|
|
4827
|
+
const manifestContent = await readFileContentOrNull((0, node_path.join)(outputRoot, ROVODEV_DIR, ROVODEV_PROMPTS_FILE_NAME));
|
|
4828
|
+
if (!manifestContent) return "";
|
|
4829
|
+
let parsed;
|
|
4830
|
+
try {
|
|
4831
|
+
parsed = (0, js_yaml.load)(manifestContent);
|
|
4832
|
+
} catch {
|
|
4833
|
+
return "";
|
|
4834
|
+
}
|
|
4835
|
+
if (!isPlainObject(parsed) || !Array.isArray(parsed.prompts)) return "";
|
|
4836
|
+
const expectedContentFile = toPosixPath((0, node_path.join)("prompts", relativeFilePath));
|
|
4837
|
+
const entry = parsed.prompts.find((candidate) => isRecord(candidate) && (candidate.content_file === expectedContentFile || candidate.name === name));
|
|
4838
|
+
return isRecord(entry) && typeof entry.description === "string" ? entry.description : "";
|
|
4839
|
+
}
|
|
4840
|
+
/**
|
|
4841
|
+
* The shared `.rovodev/prompts.yml` manifest that indexes every saved prompt.
|
|
4842
|
+
* Never deleted by orphan cleanup: it is regenerated (not individually
|
|
4843
|
+
* discovered) alongside the per-prompt content files it references.
|
|
4844
|
+
*/
|
|
4845
|
+
var RovodevPromptsManifest = class extends ToolFile {
|
|
4846
|
+
isDeletable() {
|
|
4847
|
+
return false;
|
|
4848
|
+
}
|
|
4849
|
+
validate() {
|
|
4850
|
+
return {
|
|
4851
|
+
success: true,
|
|
4852
|
+
error: null
|
|
4853
|
+
};
|
|
4854
|
+
}
|
|
4855
|
+
};
|
|
4856
|
+
//#endregion
|
|
4430
4857
|
//#region src/constants/takt-paths.ts
|
|
4431
4858
|
const TAKT_DIR = ".takt";
|
|
4432
4859
|
const TAKT_FACETS_SUBDIR = "facets";
|
|
@@ -4444,6 +4871,14 @@ const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
|
|
|
4444
4871
|
* @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
|
|
4445
4872
|
*/
|
|
4446
4873
|
const TAKT_CONFIG_FILE_NAME = "config.yaml";
|
|
4874
|
+
/**
|
|
4875
|
+
* Top-level key in Takt's `config.yaml` holding the workflow MCP transport
|
|
4876
|
+
* allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
|
|
4877
|
+
* transport must be explicitly enabled here before any workflow-defined MCP
|
|
4878
|
+
* server using it is permitted to run.
|
|
4879
|
+
* @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
|
|
4880
|
+
*/
|
|
4881
|
+
const TAKT_WORKFLOW_MCP_SERVERS_KEY = "workflow_mcp_servers";
|
|
4447
4882
|
//#endregion
|
|
4448
4883
|
//#region src/features/takt-shared.ts
|
|
4449
4884
|
/**
|
|
@@ -4619,6 +5054,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
|
|
|
4619
5054
|
supportsSubdirectory: false
|
|
4620
5055
|
}
|
|
4621
5056
|
}],
|
|
5057
|
+
["antigravity-cli", {
|
|
5058
|
+
class: AntigravityCliCommand,
|
|
5059
|
+
meta: {
|
|
5060
|
+
extension: "md",
|
|
5061
|
+
supportsProject: true,
|
|
5062
|
+
supportsGlobal: true,
|
|
5063
|
+
isSimulated: false,
|
|
5064
|
+
supportsSubdirectory: false
|
|
5065
|
+
}
|
|
5066
|
+
}],
|
|
4622
5067
|
["antigravity-ide", {
|
|
4623
5068
|
class: AntigravityIdeCommand,
|
|
4624
5069
|
meta: {
|
|
@@ -4764,7 +5209,7 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
|
|
|
4764
5209
|
meta: {
|
|
4765
5210
|
extension: "md",
|
|
4766
5211
|
supportsProject: true,
|
|
4767
|
-
supportsGlobal:
|
|
5212
|
+
supportsGlobal: true,
|
|
4768
5213
|
isSimulated: false,
|
|
4769
5214
|
supportsSubdirectory: false
|
|
4770
5215
|
}
|
|
@@ -4809,6 +5254,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
|
|
|
4809
5254
|
supportsSubdirectory: true
|
|
4810
5255
|
}
|
|
4811
5256
|
}],
|
|
5257
|
+
["reasonix", {
|
|
5258
|
+
class: ReasonixCommand,
|
|
5259
|
+
meta: {
|
|
5260
|
+
extension: "md",
|
|
5261
|
+
supportsProject: true,
|
|
5262
|
+
supportsGlobal: true,
|
|
5263
|
+
isSimulated: false,
|
|
5264
|
+
supportsSubdirectory: true
|
|
5265
|
+
}
|
|
5266
|
+
}],
|
|
4812
5267
|
["roo", {
|
|
4813
5268
|
class: RooCommand,
|
|
4814
5269
|
meta: {
|
|
@@ -4819,6 +5274,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
|
|
|
4819
5274
|
supportsSubdirectory: true
|
|
4820
5275
|
}
|
|
4821
5276
|
}],
|
|
5277
|
+
["rovodev", {
|
|
5278
|
+
class: RovodevCommand,
|
|
5279
|
+
meta: {
|
|
5280
|
+
extension: "md",
|
|
5281
|
+
supportsProject: true,
|
|
5282
|
+
supportsGlobal: true,
|
|
5283
|
+
isSimulated: false,
|
|
5284
|
+
supportsSubdirectory: false
|
|
5285
|
+
}
|
|
5286
|
+
}],
|
|
4822
5287
|
["takt", {
|
|
4823
5288
|
class: TaktCommand,
|
|
4824
5289
|
meta: {
|
|
@@ -4876,7 +5341,7 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
4876
5341
|
const rulesyncCommands = rulesyncFiles.filter((file) => file instanceof RulesyncCommand);
|
|
4877
5342
|
const factory = this.getFactory(this.toolTarget);
|
|
4878
5343
|
const flattenedPathOrigins = /* @__PURE__ */ new Map();
|
|
4879
|
-
|
|
5344
|
+
const toolCommands = rulesyncCommands.map((rulesyncCommand) => {
|
|
4880
5345
|
if (!factory.class.isTargetedByRulesyncCommand(rulesyncCommand)) return null;
|
|
4881
5346
|
const originalRelativePath = rulesyncCommand.getRelativeFilePath();
|
|
4882
5347
|
const commandToConvert = factory.meta.supportsSubdirectory ? rulesyncCommand : this.flattenRelativeFilePath(rulesyncCommand);
|
|
@@ -4892,6 +5357,14 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
4892
5357
|
global: this.global
|
|
4893
5358
|
});
|
|
4894
5359
|
}).filter((command) => command !== null);
|
|
5360
|
+
const auxiliaryFiles = await factory.class.getAuxiliaryFiles?.({
|
|
5361
|
+
toolCommands,
|
|
5362
|
+
outputRoot: this.outputRoot,
|
|
5363
|
+
global: this.global
|
|
5364
|
+
});
|
|
5365
|
+
const result = [...toolCommands];
|
|
5366
|
+
if (auxiliaryFiles && auxiliaryFiles.length > 0) result.push(...auxiliaryFiles);
|
|
5367
|
+
return result;
|
|
4895
5368
|
}
|
|
4896
5369
|
async convertToolFilesToRulesyncFiles(toolFiles) {
|
|
4897
5370
|
return toolFiles.filter((file) => file instanceof ToolCommand).map((toolCommand) => {
|
|
@@ -5050,7 +5523,14 @@ const HookDefinitionSchema = zod_mini.z.looseObject({
|
|
|
5050
5523
|
name: zod_mini.z.optional(safeString),
|
|
5051
5524
|
description: zod_mini.z.optional(safeString),
|
|
5052
5525
|
failClosed: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5053
|
-
sequential: zod_mini.z.optional(zod_mini.z.boolean())
|
|
5526
|
+
sequential: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5527
|
+
async: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5528
|
+
env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), safeString)),
|
|
5529
|
+
shell: zod_mini.z.optional(safeString),
|
|
5530
|
+
statusMessage: zod_mini.z.optional(safeString),
|
|
5531
|
+
headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), safeString)),
|
|
5532
|
+
allowedEnvVars: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
5533
|
+
once: zod_mini.z.optional(zod_mini.z.boolean())
|
|
5054
5534
|
});
|
|
5055
5535
|
/** Hook events supported by Cursor. */
|
|
5056
5536
|
const CURSOR_HOOK_EVENTS = [
|
|
@@ -5114,6 +5594,30 @@ const CLAUDE_HOOK_EVENTS = [
|
|
|
5114
5594
|
"elicitation",
|
|
5115
5595
|
"elicitationResult"
|
|
5116
5596
|
];
|
|
5597
|
+
/**
|
|
5598
|
+
* Hook events supported by Devin Local (native `.devin/` hooks).
|
|
5599
|
+
*
|
|
5600
|
+
* Devin Local adopts a Claude-Code-style lifecycle hooks surface. It documents
|
|
5601
|
+
* seven events: `PreToolUse`, `PostToolUse`, `PermissionRequest`,
|
|
5602
|
+
* `UserPromptSubmit`, `Stop`, `SessionStart`, and `SessionEnd`. The
|
|
5603
|
+
* tool/permission events (`PreToolUse`/`PostToolUse`/`PermissionRequest`) carry
|
|
5604
|
+
* a `matcher` (regex against `tool_name`); the session/turn events do not.
|
|
5605
|
+
*
|
|
5606
|
+
* Hooks live in `.devin/hooks.v1.json` (project, standalone — the hooks object
|
|
5607
|
+
* is the entire file) or under the `"hooks"` key of `.devin/config.json` /
|
|
5608
|
+
* `~/.config/devin/config.json`.
|
|
5609
|
+
*
|
|
5610
|
+
* @see https://docs.devin.ai/cli/extensibility/hooks/overview
|
|
5611
|
+
*/
|
|
5612
|
+
const DEVIN_HOOK_EVENTS = [
|
|
5613
|
+
"sessionStart",
|
|
5614
|
+
"sessionEnd",
|
|
5615
|
+
"preToolUse",
|
|
5616
|
+
"postToolUse",
|
|
5617
|
+
"beforeSubmitPrompt",
|
|
5618
|
+
"stop",
|
|
5619
|
+
"permissionRequest"
|
|
5620
|
+
];
|
|
5117
5621
|
/** Hook events supported by OpenCode. */
|
|
5118
5622
|
const OPENCODE_HOOK_EVENTS = [
|
|
5119
5623
|
"sessionStart",
|
|
@@ -5156,7 +5660,11 @@ const COPILOT_HOOK_EVENTS = [
|
|
|
5156
5660
|
* `sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
|
|
5157
5661
|
* `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
|
|
5158
5662
|
* `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
|
|
5159
|
-
* `notification`.
|
|
5663
|
+
* `notification`, `preMcpToolCall` ← `beforeMCPExecution`.
|
|
5664
|
+
*
|
|
5665
|
+
* `preMcpToolCall` (canonical `beforeMCPExecution`) was added in Copilot CLI
|
|
5666
|
+
* v1.0.51 (2026-05-20) for hook providers to control outgoing MCP request
|
|
5667
|
+
* metadata. https://github.com/github/copilot-cli/blob/main/changelog.md
|
|
5160
5668
|
*
|
|
5161
5669
|
* @see https://docs.github.com/en/copilot/reference/hooks-configuration
|
|
5162
5670
|
*/
|
|
@@ -5173,7 +5681,8 @@ const COPILOTCLI_HOOK_EVENTS = [
|
|
|
5173
5681
|
"afterError",
|
|
5174
5682
|
"preCompact",
|
|
5175
5683
|
"permissionRequest",
|
|
5176
|
-
"notification"
|
|
5684
|
+
"notification",
|
|
5685
|
+
"beforeMCPExecution"
|
|
5177
5686
|
];
|
|
5178
5687
|
/**
|
|
5179
5688
|
* Hook events supported by Factory Droid.
|
|
@@ -5258,6 +5767,25 @@ const KIRO_HOOK_EVENTS = [
|
|
|
5258
5767
|
"stop"
|
|
5259
5768
|
];
|
|
5260
5769
|
/**
|
|
5770
|
+
* Hook events supported by the Kiro IDE (`.kiro/hooks/*.json` v1).
|
|
5771
|
+
*
|
|
5772
|
+
* Kiro IDE 1.0 exposes PascalCase triggers. rulesync maps the canonical
|
|
5773
|
+
* lifecycle events that have a clean 1:1 IDE equivalent: `SessionStart`,
|
|
5774
|
+
* `Stop`, `UserPromptSubmit`, `PreToolUse`, and `PostToolUse`. The IDE also
|
|
5775
|
+
* documents file-event (`PostFileCreate`/`PostFileSave`/`PostFileDelete`) and
|
|
5776
|
+
* spec-task (`PreTaskExec`/`PostTaskExec`) triggers that have no canonical
|
|
5777
|
+
* equivalent; those can still be emitted verbatim via a `kiro-ide` override
|
|
5778
|
+
* block (unknown event keys pass through unchanged).
|
|
5779
|
+
* @see https://kiro.dev/docs/hooks/types/
|
|
5780
|
+
*/
|
|
5781
|
+
const KIRO_IDE_HOOK_EVENTS = [
|
|
5782
|
+
"sessionStart",
|
|
5783
|
+
"beforeSubmitPrompt",
|
|
5784
|
+
"preToolUse",
|
|
5785
|
+
"postToolUse",
|
|
5786
|
+
"stop"
|
|
5787
|
+
];
|
|
5788
|
+
/**
|
|
5261
5789
|
* Hook events supported by Google Antigravity (both the IDE and the CLI).
|
|
5262
5790
|
*
|
|
5263
5791
|
* Antigravity exposes a Claude-style hooks surface covering the five
|
|
@@ -5308,18 +5836,23 @@ const VIBE_HOOK_EVENTS = [
|
|
|
5308
5836
|
/**
|
|
5309
5837
|
* Hook events supported by JetBrains Junie CLI.
|
|
5310
5838
|
*
|
|
5311
|
-
* Junie CLI exposes
|
|
5312
|
-
* `~/.junie/config.json`: `SessionStart`, `UserPromptSubmit`, `
|
|
5313
|
-
* `
|
|
5314
|
-
*
|
|
5315
|
-
*
|
|
5316
|
-
*
|
|
5839
|
+
* Junie CLI exposes seven lifecycle events under the `"hooks"` key of
|
|
5840
|
+
* `~/.junie/config.json`: `SessionStart`, `UserPromptSubmit`, `PreToolUse`,
|
|
5841
|
+
* `Stop`, `StopFailure`, `PermissionRequest`, and `SessionEnd`. Matchers apply
|
|
5842
|
+
* to `SessionStart` (source), `PreToolUse` (tool name), `StopFailure` (error
|
|
5843
|
+
* type), `PermissionRequest` (tool name), and `SessionEnd` (reason);
|
|
5844
|
+
* `UserPromptSubmit` and `Stop` are matcher-less and always run. Only
|
|
5845
|
+
* `type: "command"` hooks are supported. Project-local hooks are ignored for
|
|
5846
|
+
* safety.
|
|
5317
5847
|
* @see https://junie.jetbrains.com/docs/junie-cli-hooks.html
|
|
5318
5848
|
*/
|
|
5319
5849
|
const JUNIE_HOOK_EVENTS = [
|
|
5320
5850
|
"sessionStart",
|
|
5321
5851
|
"beforeSubmitPrompt",
|
|
5852
|
+
"preToolUse",
|
|
5322
5853
|
"stop",
|
|
5854
|
+
"stopFailure",
|
|
5855
|
+
"permissionRequest",
|
|
5323
5856
|
"sessionEnd"
|
|
5324
5857
|
];
|
|
5325
5858
|
/**
|
|
@@ -5351,6 +5884,69 @@ const QWENCODE_HOOK_EVENTS = [
|
|
|
5351
5884
|
"todoCreated",
|
|
5352
5885
|
"todoCompleted"
|
|
5353
5886
|
];
|
|
5887
|
+
/**
|
|
5888
|
+
* Hook events supported by Reasonix.
|
|
5889
|
+
*
|
|
5890
|
+
* Reasonix's `.reasonix/settings.json` (project) / `~/.reasonix/settings.json`
|
|
5891
|
+
* (global) documents a ten-event surface (`PreToolUse`, `PostToolUse`,
|
|
5892
|
+
* `UserPromptSubmit`, `Stop`, `PostLLMCall`, `SessionStart`, `SessionEnd`,
|
|
5893
|
+
* `SubagentStop`, `Notification`, `PreCompact`), but only the four events the
|
|
5894
|
+
* upstream issue scoped in are mapped here: `PreToolUse`, `PostToolUse`,
|
|
5895
|
+
* `UserPromptSubmit` ← `beforeSubmitPrompt`, and `Stop`. `match` (Reasonix's
|
|
5896
|
+
* matcher field name) is honored only on `PreToolUse`/`PostToolUse`, matching
|
|
5897
|
+
* the canonical `matcher` field's tool-event scoping used by other adapters.
|
|
5898
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
|
|
5899
|
+
*/
|
|
5900
|
+
const REASONIX_HOOK_EVENTS = [
|
|
5901
|
+
"preToolUse",
|
|
5902
|
+
"postToolUse",
|
|
5903
|
+
"beforeSubmitPrompt",
|
|
5904
|
+
"stop"
|
|
5905
|
+
];
|
|
5906
|
+
/**
|
|
5907
|
+
* Hook events supported by Hermes Agent's native Shell Hooks system.
|
|
5908
|
+
*
|
|
5909
|
+
* Hermes validates hook events against a fixed `VALID_HOOKS` set:
|
|
5910
|
+
* `pre_tool_call`, `post_tool_call`, `pre_llm_call`, `post_llm_call`,
|
|
5911
|
+
* `pre_verify`, `on_session_start`, `on_session_end`, `on_session_finalize`,
|
|
5912
|
+
* `on_session_reset`, `subagent_start`, `subagent_stop`, `pre_gateway_dispatch`,
|
|
5913
|
+
* `pre_approval_request`, `post_approval_response`, `transform_tool_result`,
|
|
5914
|
+
* `transform_terminal_output`, `transform_llm_output`. Only the events with a
|
|
5915
|
+
* clean 1:1 canonical equivalent are mapped here; the remaining `VALID_HOOKS`
|
|
5916
|
+
* entries (`pre_verify`, `on_session_finalize`, `on_session_reset`,
|
|
5917
|
+
* `pre_gateway_dispatch`, `pre_approval_request`, `post_approval_response`, the
|
|
5918
|
+
* `transform_*` result-rewriting hooks) have no canonical rulesync equivalent,
|
|
5919
|
+
* so no canonical event maps to them.
|
|
5920
|
+
* @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
|
|
5921
|
+
*/
|
|
5922
|
+
const HERMESAGENT_HOOK_EVENTS = [
|
|
5923
|
+
"sessionStart",
|
|
5924
|
+
"sessionEnd",
|
|
5925
|
+
"preToolUse",
|
|
5926
|
+
"postToolUse",
|
|
5927
|
+
"preModelInvocation",
|
|
5928
|
+
"postModelInvocation",
|
|
5929
|
+
"subagentStart",
|
|
5930
|
+
"subagentStop"
|
|
5931
|
+
];
|
|
5932
|
+
/**
|
|
5933
|
+
* Map canonical camelCase event names to Hermes Agent's native `VALID_HOOKS`
|
|
5934
|
+
* snake_case keys under the `hooks:` block of `~/.hermes/config.yaml`.
|
|
5935
|
+
*/
|
|
5936
|
+
const CANONICAL_TO_HERMESAGENT_EVENT_NAMES = {
|
|
5937
|
+
sessionStart: "on_session_start",
|
|
5938
|
+
sessionEnd: "on_session_end",
|
|
5939
|
+
preToolUse: "pre_tool_call",
|
|
5940
|
+
postToolUse: "post_tool_call",
|
|
5941
|
+
preModelInvocation: "pre_llm_call",
|
|
5942
|
+
postModelInvocation: "post_llm_call",
|
|
5943
|
+
subagentStart: "subagent_start",
|
|
5944
|
+
subagentStop: "subagent_stop"
|
|
5945
|
+
};
|
|
5946
|
+
/**
|
|
5947
|
+
* Map Hermes Agent's native `VALID_HOOKS` keys back to canonical camelCase.
|
|
5948
|
+
*/
|
|
5949
|
+
const HERMESAGENT_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_HERMESAGENT_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5354
5950
|
const hooksRecordSchema = zod_mini.z.record(zod_mini.z.string(), zod_mini.z.array(HookDefinitionSchema));
|
|
5355
5951
|
/**
|
|
5356
5952
|
* Canonical hooks config (canonical event names in camelCase).
|
|
@@ -5370,12 +5966,15 @@ const HooksConfigSchema = zod_mini.z.looseObject({
|
|
|
5370
5966
|
deepagents: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5371
5967
|
kiro: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5372
5968
|
"kiro-cli": zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5969
|
+
"kiro-ide": zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5373
5970
|
devin: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5374
5971
|
augmentcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5375
5972
|
"antigravity-ide": zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5376
5973
|
"antigravity-cli": zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5974
|
+
hermesagent: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5377
5975
|
junie: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5378
5976
|
vibe: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5977
|
+
reasonix: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5379
5978
|
qwencode: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
5380
5979
|
hooks: zod_mini.z.optional(hooksRecordSchema),
|
|
5381
5980
|
disableAllHooks: zod_mini.z.optional(zod_mini.z.boolean())
|
|
@@ -5421,6 +6020,26 @@ const CANONICAL_TO_CLAUDE_EVENT_NAMES = {
|
|
|
5421
6020
|
*/
|
|
5422
6021
|
const CLAUDE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CLAUDE_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5423
6022
|
/**
|
|
6023
|
+
* Map canonical camelCase event names to Devin Local PascalCase.
|
|
6024
|
+
*
|
|
6025
|
+
* Devin Local reuses the same Claude-style PascalCase event names for the
|
|
6026
|
+
* subset of events it supports.
|
|
6027
|
+
* @see https://docs.devin.ai/cli/extensibility/hooks/overview
|
|
6028
|
+
*/
|
|
6029
|
+
const CANONICAL_TO_DEVIN_EVENT_NAMES = {
|
|
6030
|
+
sessionStart: "SessionStart",
|
|
6031
|
+
sessionEnd: "SessionEnd",
|
|
6032
|
+
preToolUse: "PreToolUse",
|
|
6033
|
+
postToolUse: "PostToolUse",
|
|
6034
|
+
beforeSubmitPrompt: "UserPromptSubmit",
|
|
6035
|
+
stop: "Stop",
|
|
6036
|
+
permissionRequest: "PermissionRequest"
|
|
6037
|
+
};
|
|
6038
|
+
/**
|
|
6039
|
+
* Map Devin Local PascalCase event names to canonical camelCase.
|
|
6040
|
+
*/
|
|
6041
|
+
const DEVIN_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_DEVIN_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
6042
|
+
/**
|
|
5424
6043
|
* Map canonical camelCase event names to AugmentCode PascalCase.
|
|
5425
6044
|
* Auggie reuses the same PascalCase names as Claude for the events it supports.
|
|
5426
6045
|
*/
|
|
@@ -5551,7 +6170,8 @@ const CANONICAL_TO_COPILOTCLI_EVENT_NAMES = {
|
|
|
5551
6170
|
afterError: "errorOccurred",
|
|
5552
6171
|
preCompact: "preCompact",
|
|
5553
6172
|
permissionRequest: "permissionRequest",
|
|
5554
|
-
notification: "notification"
|
|
6173
|
+
notification: "notification",
|
|
6174
|
+
beforeMCPExecution: "preMcpToolCall"
|
|
5555
6175
|
};
|
|
5556
6176
|
/** Map GitHub Copilot CLI event names back to canonical camelCase. */
|
|
5557
6177
|
const COPILOTCLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_COPILOTCLI_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
@@ -5633,13 +6253,35 @@ const CANONICAL_TO_KIRO_EVENT_NAMES = {
|
|
|
5633
6253
|
*/
|
|
5634
6254
|
const KIRO_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_KIRO_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5635
6255
|
/**
|
|
6256
|
+
* Map canonical camelCase event names to Kiro IDE PascalCase triggers.
|
|
6257
|
+
*
|
|
6258
|
+
* Only the canonical lifecycle events with a clean IDE equivalent are mapped.
|
|
6259
|
+
* Unknown keys (e.g. IDE-only `PostFileSave`/`PreTaskExec` set via a `kiro-ide`
|
|
6260
|
+
* override) pass through unchanged.
|
|
6261
|
+
* @see https://kiro.dev/docs/hooks/types/
|
|
6262
|
+
*/
|
|
6263
|
+
const CANONICAL_TO_KIRO_IDE_EVENT_NAMES = {
|
|
6264
|
+
sessionStart: "SessionStart",
|
|
6265
|
+
beforeSubmitPrompt: "UserPromptSubmit",
|
|
6266
|
+
preToolUse: "PreToolUse",
|
|
6267
|
+
postToolUse: "PostToolUse",
|
|
6268
|
+
stop: "Stop"
|
|
6269
|
+
};
|
|
6270
|
+
/**
|
|
6271
|
+
* Map Kiro IDE PascalCase trigger names to canonical camelCase.
|
|
6272
|
+
*/
|
|
6273
|
+
const KIRO_IDE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_KIRO_IDE_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
6274
|
+
/**
|
|
5636
6275
|
* Map canonical camelCase event names to Junie PascalCase.
|
|
5637
6276
|
* Junie reuses the same PascalCase names as Claude for the events it supports.
|
|
5638
6277
|
*/
|
|
5639
6278
|
const CANONICAL_TO_JUNIE_EVENT_NAMES = {
|
|
5640
6279
|
sessionStart: "SessionStart",
|
|
5641
6280
|
beforeSubmitPrompt: "UserPromptSubmit",
|
|
6281
|
+
preToolUse: "PreToolUse",
|
|
5642
6282
|
stop: "Stop",
|
|
6283
|
+
stopFailure: "StopFailure",
|
|
6284
|
+
permissionRequest: "PermissionRequest",
|
|
5643
6285
|
sessionEnd: "SessionEnd"
|
|
5644
6286
|
};
|
|
5645
6287
|
/**
|
|
@@ -5692,19 +6334,22 @@ const CANONICAL_TO_QWENCODE_EVENT_NAMES = {
|
|
|
5692
6334
|
* Map Qwen Code PascalCase event names to canonical camelCase.
|
|
5693
6335
|
*/
|
|
5694
6336
|
const QWENCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_QWENCODE_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
const
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
6337
|
+
/**
|
|
6338
|
+
* Map canonical camelCase event names to Reasonix PascalCase.
|
|
6339
|
+
* Reasonix explicitly mirrors Claude Code's hooks model, so it reuses the same
|
|
6340
|
+
* PascalCase names for the four events rulesync maps.
|
|
6341
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
|
|
6342
|
+
*/
|
|
6343
|
+
const CANONICAL_TO_REASONIX_EVENT_NAMES = {
|
|
6344
|
+
preToolUse: "PreToolUse",
|
|
6345
|
+
postToolUse: "PostToolUse",
|
|
6346
|
+
beforeSubmitPrompt: "UserPromptSubmit",
|
|
6347
|
+
stop: "Stop"
|
|
6348
|
+
};
|
|
6349
|
+
/**
|
|
6350
|
+
* Map Reasonix PascalCase event names to canonical camelCase.
|
|
6351
|
+
*/
|
|
6352
|
+
const REASONIX_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5708
6353
|
//#endregion
|
|
5709
6354
|
//#region src/utils/prototype-pollution.ts
|
|
5710
6355
|
/**
|
|
@@ -5752,7 +6397,7 @@ function isToolMatcherEntry(x) {
|
|
|
5752
6397
|
/**
|
|
5753
6398
|
* Filter the shared canonical hooks to the supported events and merge tool overrides on top.
|
|
5754
6399
|
*/
|
|
5755
|
-
function buildEffectiveHooks({ config, toolOverrideHooks, supportedEvents }) {
|
|
6400
|
+
function buildEffectiveHooks$1({ config, toolOverrideHooks, supportedEvents }) {
|
|
5756
6401
|
const supported = new Set(supportedEvents);
|
|
5757
6402
|
const sharedHooks = {};
|
|
5758
6403
|
for (const [event, defs] of Object.entries(config.hooks)) if (supported.has(event)) sharedHooks[event] = defs;
|
|
@@ -5781,7 +6426,7 @@ function groupDefinitionsByMatcher(definitions) {
|
|
|
5781
6426
|
function applyCommandPrefix({ def, converterConfig }) {
|
|
5782
6427
|
const commandText = def.command;
|
|
5783
6428
|
const trimmedCommand = typeof commandText === "string" ? commandText.trimStart() : void 0;
|
|
5784
|
-
return converterConfig.projectDirVar !== "" && typeof trimmedCommand === "string" && !trimmedCommand.startsWith("$") && (!converterConfig.prefixDotRelativeCommandsOnly || trimmedCommand.startsWith(".")) && typeof trimmedCommand === "string" ?
|
|
6429
|
+
return converterConfig.projectDirVar !== "" && typeof trimmedCommand === "string" && !trimmedCommand.startsWith("$") && (!converterConfig.prefixDotRelativeCommandsOnly || trimmedCommand.startsWith(".")) && typeof trimmedCommand === "string" ? `"${converterConfig.projectDirVar}"/${trimmedCommand.replace(/^\.\//, "")}` : def.command;
|
|
5785
6430
|
}
|
|
5786
6431
|
/**
|
|
5787
6432
|
* Convert the definitions of a single matcher group into tool hook entries,
|
|
@@ -5814,7 +6459,7 @@ function buildToolHooks({ defs, converterConfig }) {
|
|
|
5814
6459
|
* (e.g. beforeSubmitPrompt → UserPromptSubmit).
|
|
5815
6460
|
*/
|
|
5816
6461
|
function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logger }) {
|
|
5817
|
-
const effectiveHooks = buildEffectiveHooks({
|
|
6462
|
+
const effectiveHooks = buildEffectiveHooks$1({
|
|
5818
6463
|
config,
|
|
5819
6464
|
toolOverrideHooks,
|
|
5820
6465
|
supportedEvents: converterConfig.supportedEvents
|
|
@@ -5857,7 +6502,13 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
|
|
|
5857
6502
|
*/
|
|
5858
6503
|
function stripCommandPrefix({ command, converterConfig }) {
|
|
5859
6504
|
const cmd = typeof command === "string" ? command : void 0;
|
|
5860
|
-
if (converterConfig.projectDirVar
|
|
6505
|
+
if (converterConfig.projectDirVar === "" || typeof cmd !== "string") return cmd;
|
|
6506
|
+
const quotedPrefix = `"${converterConfig.projectDirVar}"/`;
|
|
6507
|
+
if (cmd.startsWith(quotedPrefix)) return `./${cmd.slice(quotedPrefix.length)}`;
|
|
6508
|
+
if (cmd.includes(`${converterConfig.projectDirVar}/`)) {
|
|
6509
|
+
const escapedVar = converterConfig.projectDirVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6510
|
+
return cmd.replace(new RegExp(`^${escapedVar}\\/?`), "./");
|
|
6511
|
+
}
|
|
5861
6512
|
return cmd;
|
|
5862
6513
|
}
|
|
5863
6514
|
/**
|
|
@@ -5910,9 +6561,6 @@ function toolHooksToCanonical({ hooks, converterConfig }) {
|
|
|
5910
6561
|
return canonical;
|
|
5911
6562
|
}
|
|
5912
6563
|
//#endregion
|
|
5913
|
-
//#region src/types/tool-file.ts
|
|
5914
|
-
var ToolFile = class extends AiFile {};
|
|
5915
|
-
//#endregion
|
|
5916
6564
|
//#region src/features/hooks/rulesync-hooks.ts
|
|
5917
6565
|
var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
|
|
5918
6566
|
json;
|
|
@@ -6466,9 +7114,15 @@ const CODEXCLI_CONVERTER_CONFIG = {
|
|
|
6466
7114
|
passthroughFields: ["name", "description"]
|
|
6467
7115
|
};
|
|
6468
7116
|
/**
|
|
6469
|
-
* Build the content for `.codex/config.toml
|
|
6470
|
-
* Reads the existing file (if any), parses TOML,
|
|
6471
|
-
*
|
|
7117
|
+
* Build the content for `.codex/config.toml`, cleaning up the deprecated `codex_hooks` key.
|
|
7118
|
+
* Reads the existing file (if any), parses TOML, and returns the content without writing to
|
|
7119
|
+
* disk. The caller is responsible for writing via the normal write phase.
|
|
7120
|
+
*
|
|
7121
|
+
* Hooks are GA and enabled by default in Codex CLI, so `[features] hooks = true` is no longer
|
|
7122
|
+
* required and is intentionally NOT force-written here (doing so used to be harmless/idempotent,
|
|
7123
|
+
* but is now redundant and could mask a user's own `hooks = false` opt-out on a later edit).
|
|
7124
|
+
* See https://developers.openai.com/codex/hooks. Only the legacy `codex_hooks` alias — superseded
|
|
7125
|
+
* by `hooks` — is still cleaned up.
|
|
6472
7126
|
*/
|
|
6473
7127
|
async function buildCodexConfigTomlContent({ outputRoot }) {
|
|
6474
7128
|
const configPath = (0, node_path.join)(outputRoot, CODEXCLI_DIR, CODEXCLI_MCP_FILE_NAME);
|
|
@@ -6479,10 +7133,7 @@ async function buildCodexConfigTomlContent({ outputRoot }) {
|
|
|
6479
7133
|
} catch (error) {
|
|
6480
7134
|
throw new Error(`Failed to parse existing Codex CLI config at ${configPath}: ${formatError(error)}`, { cause: error });
|
|
6481
7135
|
}
|
|
6482
|
-
if (typeof configToml.features
|
|
6483
|
-
const features = configToml.features;
|
|
6484
|
-
delete features.codex_hooks;
|
|
6485
|
-
features.hooks = true;
|
|
7136
|
+
if (typeof configToml.features === "object" && configToml.features !== null) delete configToml.features.codex_hooks;
|
|
6486
7137
|
return smol_toml.stringify(configToml);
|
|
6487
7138
|
}
|
|
6488
7139
|
/**
|
|
@@ -6750,6 +7401,20 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
|
|
|
6750
7401
|
}
|
|
6751
7402
|
};
|
|
6752
7403
|
//#endregion
|
|
7404
|
+
//#region src/utils/object.ts
|
|
7405
|
+
/**
|
|
7406
|
+
* Return a shallow copy of `obj` keeping only the entries whose value is
|
|
7407
|
+
* neither `undefined` nor `null`.
|
|
7408
|
+
*
|
|
7409
|
+
* Used to assemble generated config objects without one conditional spread per
|
|
7410
|
+
* optional field (which would otherwise exceed the lint complexity budget).
|
|
7411
|
+
*/
|
|
7412
|
+
function compact(obj) {
|
|
7413
|
+
const result = {};
|
|
7414
|
+
for (const [key, value] of Object.entries(obj)) if (value !== void 0 && value !== null) result[key] = value;
|
|
7415
|
+
return result;
|
|
7416
|
+
}
|
|
7417
|
+
//#endregion
|
|
6753
7418
|
//#region src/features/hooks/copilotcli-hooks.ts
|
|
6754
7419
|
/**
|
|
6755
7420
|
* GitHub Copilot CLI hooks.
|
|
@@ -6761,7 +7426,7 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
|
|
|
6761
7426
|
* (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
|
|
6762
7427
|
* `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
|
|
6763
7428
|
* `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
|
|
6764
|
-
* `notification`). Each entry supports three hook types:
|
|
7429
|
+
* `notification`, `preMcpToolCall`). Each entry supports three hook types:
|
|
6765
7430
|
*
|
|
6766
7431
|
* - `command` — the `bash` / `powershell` command-field shape with optional
|
|
6767
7432
|
* `timeoutSec`, plus optional `cwd` / `env`.
|
|
@@ -6874,20 +7539,24 @@ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchem
|
|
|
6874
7539
|
} else if (hookType === "http") entries.push({
|
|
6875
7540
|
type: "http",
|
|
6876
7541
|
...matcherPart,
|
|
6877
|
-
...
|
|
7542
|
+
...compact({
|
|
7543
|
+
url: def.url,
|
|
7544
|
+
headers: def.headers,
|
|
7545
|
+
allowedEnvVars: def.allowedEnvVars
|
|
7546
|
+
}),
|
|
7547
|
+
...timeoutPart,
|
|
7548
|
+
...rest
|
|
7549
|
+
});
|
|
7550
|
+
else entries.push({
|
|
7551
|
+
type: "command",
|
|
7552
|
+
...matcherPart,
|
|
7553
|
+
...compact({
|
|
7554
|
+
[commandField]: def.command,
|
|
7555
|
+
env: def.env
|
|
7556
|
+
}),
|
|
6878
7557
|
...timeoutPart,
|
|
6879
7558
|
...rest
|
|
6880
7559
|
});
|
|
6881
|
-
else {
|
|
6882
|
-
const command = def.command;
|
|
6883
|
-
entries.push({
|
|
6884
|
-
type: "command",
|
|
6885
|
-
...matcherPart,
|
|
6886
|
-
...command !== void 0 && command !== null && { [commandField]: command },
|
|
6887
|
-
...timeoutPart,
|
|
6888
|
-
...rest
|
|
6889
|
-
});
|
|
6890
|
-
}
|
|
6891
7560
|
}
|
|
6892
7561
|
return entries;
|
|
6893
7562
|
}
|
|
@@ -7296,148 +7965,63 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
|
|
|
7296
7965
|
};
|
|
7297
7966
|
//#endregion
|
|
7298
7967
|
//#region src/features/hooks/devin-hooks.ts
|
|
7968
|
+
const DEVIN_CONVERTER_CONFIG = {
|
|
7969
|
+
supportedEvents: DEVIN_HOOK_EVENTS,
|
|
7970
|
+
canonicalToToolEventNames: CANONICAL_TO_DEVIN_EVENT_NAMES,
|
|
7971
|
+
toolToCanonicalEventNames: DEVIN_TO_CANONICAL_EVENT_NAMES,
|
|
7972
|
+
projectDirVar: "",
|
|
7973
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command", "prompt"]),
|
|
7974
|
+
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
7975
|
+
"sessionStart",
|
|
7976
|
+
"sessionEnd",
|
|
7977
|
+
"stop",
|
|
7978
|
+
"beforeSubmitPrompt"
|
|
7979
|
+
])
|
|
7980
|
+
};
|
|
7299
7981
|
/**
|
|
7300
|
-
*
|
|
7301
|
-
*
|
|
7302
|
-
* The mapping is bijective for every event with a Devin equivalent, so the
|
|
7303
|
-
* conversion round-trips cleanly. Devin splits the generic tool lifecycle
|
|
7304
|
-
* into file/command/MCP specific events, so we line them up with the closest
|
|
7305
|
-
* file/command/MCP specific canonical events rather than the generic
|
|
7306
|
-
* preToolUse/postToolUse pair:
|
|
7307
|
-
*
|
|
7308
|
-
* - beforeReadFile ⇄ pre_read_code
|
|
7309
|
-
* - beforeTabFileRead ⇄ post_read_code
|
|
7310
|
-
* - afterTabFileEdit ⇄ pre_write_code
|
|
7311
|
-
* - afterFileEdit ⇄ post_write_code
|
|
7312
|
-
* - beforeShellExecution⇄ pre_run_command
|
|
7313
|
-
* - afterShellExecution ⇄ post_run_command
|
|
7314
|
-
* - beforeMCPExecution ⇄ pre_mcp_tool_use
|
|
7315
|
-
* - afterMCPExecution ⇄ post_mcp_tool_use
|
|
7316
|
-
* - beforeSubmitPrompt ⇄ pre_user_prompt
|
|
7317
|
-
* - afterAgentResponse ⇄ post_cascade_response
|
|
7318
|
-
* - beforeAgentResponse ⇄ post_cascade_response_with_transcript
|
|
7319
|
-
* - worktreeCreate ⇄ post_setup_worktree
|
|
7320
|
-
*
|
|
7321
|
-
* NOTE on the before/after prefix mismatch: rulesync's canonical vocabulary has
|
|
7322
|
-
* no `afterReadFile` or `beforeWriteFile`/`beforeFileEdit` event — the only read
|
|
7323
|
-
* events are `beforeReadFile`/`beforeTabFileRead` (both read-side) and the only
|
|
7324
|
-
* edit events are `afterFileEdit`/`afterTabFileEdit` (both edit-side). To cover
|
|
7325
|
-
* all twelve Devin events bijectively we therefore pair the second read/write
|
|
7326
|
-
* event by DOMAIN (read↔read, write↔write) rather than by timing, which inverts
|
|
7327
|
-
* the prefix on `post_read_code` (⇄ beforeTabFileRead) and `pre_write_code`
|
|
7328
|
-
* (⇄ afterTabFileEdit). The alternative — pairing by timing — would leave two
|
|
7329
|
-
* Devin events unmapped and drop user hooks. The round-trip stays lossless;
|
|
7330
|
-
* only the human-readable prefix differs, so this is a deliberate trade-off.
|
|
7331
|
-
*
|
|
7332
|
-
* Canonical events that have no Devin equivalent (e.g. sessionStart, stop)
|
|
7333
|
-
* are dropped with a logged warning during export.
|
|
7334
|
-
*/
|
|
7335
|
-
const CANONICAL_TO_DEVIN_EVENT_NAMES = {
|
|
7336
|
-
beforeReadFile: "pre_read_code",
|
|
7337
|
-
beforeTabFileRead: "post_read_code",
|
|
7338
|
-
afterTabFileEdit: "pre_write_code",
|
|
7339
|
-
afterFileEdit: "post_write_code",
|
|
7340
|
-
beforeShellExecution: "pre_run_command",
|
|
7341
|
-
afterShellExecution: "post_run_command",
|
|
7342
|
-
beforeMCPExecution: "pre_mcp_tool_use",
|
|
7343
|
-
afterMCPExecution: "post_mcp_tool_use",
|
|
7344
|
-
beforeSubmitPrompt: "pre_user_prompt",
|
|
7345
|
-
afterAgentResponse: "post_cascade_response",
|
|
7346
|
-
beforeAgentResponse: "post_cascade_response_with_transcript",
|
|
7347
|
-
worktreeCreate: "post_setup_worktree"
|
|
7348
|
-
};
|
|
7349
|
-
/**
|
|
7350
|
-
* Map Devin event names back to canonical camelCase event names.
|
|
7351
|
-
*/
|
|
7352
|
-
const DEVIN_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_DEVIN_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
7353
|
-
/**
|
|
7354
|
-
* Canonical hook events supported by Devin (the keys of the bijective map).
|
|
7982
|
+
* Hooks generator for Devin Local (native `.devin/` hooks).
|
|
7355
7983
|
*
|
|
7356
|
-
*
|
|
7357
|
-
*
|
|
7358
|
-
*
|
|
7359
|
-
*/
|
|
7360
|
-
const DEVIN_HOOK_EVENTS = [
|
|
7361
|
-
"beforeReadFile",
|
|
7362
|
-
"beforeTabFileRead",
|
|
7363
|
-
"afterTabFileEdit",
|
|
7364
|
-
"afterFileEdit",
|
|
7365
|
-
"beforeShellExecution",
|
|
7366
|
-
"afterShellExecution",
|
|
7367
|
-
"beforeMCPExecution",
|
|
7368
|
-
"afterMCPExecution",
|
|
7369
|
-
"beforeSubmitPrompt",
|
|
7370
|
-
"afterAgentResponse",
|
|
7371
|
-
"beforeAgentResponse",
|
|
7372
|
-
"worktreeCreate"
|
|
7373
|
-
];
|
|
7374
|
-
function isRecord(value) {
|
|
7375
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7376
|
-
}
|
|
7377
|
-
/**
|
|
7378
|
-
* Read the per-tool `devin` override hooks from the rulesync hooks config.
|
|
7984
|
+
* Devin Local adopts a Claude-Code-style lifecycle hooks model: each event maps
|
|
7985
|
+
* to an array of `{ matcher?, hooks: [{ type, command|prompt, timeout? }] }`
|
|
7986
|
+
* matcher groups.
|
|
7379
7987
|
*
|
|
7380
|
-
*
|
|
7381
|
-
*
|
|
7382
|
-
* config
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
}
|
|
7387
|
-
/**
|
|
7388
|
-
* Convert a canonical hook definition into a Devin hook object, keeping only
|
|
7389
|
-
* the Devin-supported fields. Returns null when neither command nor
|
|
7390
|
-
* powershell is present (Devin requires at least one of them).
|
|
7391
|
-
*/
|
|
7392
|
-
function canonicalDefToDevinHook(def) {
|
|
7393
|
-
const hook = {};
|
|
7394
|
-
if (typeof def.command === "string") hook.command = def.command;
|
|
7395
|
-
if (typeof def.powershell === "string") hook.powershell = def.powershell;
|
|
7396
|
-
if (typeof def.show_output === "boolean") hook.show_output = def.show_output;
|
|
7397
|
-
if (typeof def.working_directory === "string") hook.working_directory = def.working_directory;
|
|
7398
|
-
if (hook.command === void 0 && hook.powershell === void 0) return null;
|
|
7399
|
-
return hook;
|
|
7400
|
-
}
|
|
7401
|
-
/**
|
|
7402
|
-
* Convert a Devin hook object into a canonical hook definition.
|
|
7403
|
-
*/
|
|
7404
|
-
function devinHookToCanonicalDef(hook) {
|
|
7405
|
-
const def = { type: "command" };
|
|
7406
|
-
if (typeof hook.command === "string") def.command = hook.command;
|
|
7407
|
-
if (typeof hook.powershell === "string") def.powershell = hook.powershell;
|
|
7408
|
-
if (typeof hook.show_output === "boolean") def.show_output = hook.show_output;
|
|
7409
|
-
if (typeof hook.working_directory === "string") def.working_directory = hook.working_directory;
|
|
7410
|
-
return def;
|
|
7411
|
-
}
|
|
7412
|
-
/**
|
|
7413
|
-
* Hooks generator for Devin Cascade (GA).
|
|
7988
|
+
* - Project scope: `.devin/hooks.v1.json`. This is a standalone file whose top
|
|
7989
|
+
* level IS the event map (no wrapper key).
|
|
7990
|
+
* - Global scope: `~/.config/devin/config.json` under the `"hooks"` key. This
|
|
7991
|
+
* file is shared with the MCP (`mcpServers`) and permissions (`permissions`)
|
|
7992
|
+
* features, so reads and writes merge into the existing JSON and the file is
|
|
7993
|
+
* never deleted in global mode.
|
|
7414
7994
|
*
|
|
7415
|
-
*
|
|
7416
|
-
* Devin event name to a flat array of hook objects. Project and global modes
|
|
7417
|
-
* share the same shape; only the location differs (`.windsurf/hooks.json` vs
|
|
7418
|
-
* `~/.codeium/windsurf/hooks.json`). The harness overrides `outputRoot` with the
|
|
7419
|
-
* home directory in global mode.
|
|
7995
|
+
* @see https://docs.devin.ai/cli/extensibility/hooks/overview
|
|
7420
7996
|
*/
|
|
7421
7997
|
var DevinHooks = class DevinHooks extends ToolHooks {
|
|
7422
7998
|
constructor(params) {
|
|
7423
7999
|
super({
|
|
7424
8000
|
...params,
|
|
7425
|
-
fileContent: params.fileContent ??
|
|
8001
|
+
fileContent: params.fileContent ?? "{}"
|
|
7426
8002
|
});
|
|
7427
8003
|
}
|
|
8004
|
+
/**
|
|
8005
|
+
* The project standalone `hooks.v1.json` is owned wholesale by rulesync and
|
|
8006
|
+
* may be deleted as an orphan. The global `config.json` is shared with the MCP
|
|
8007
|
+
* and permissions features, so it must never be deleted.
|
|
8008
|
+
*/
|
|
8009
|
+
isDeletable() {
|
|
8010
|
+
return this.getRelativeFilePath() !== DEVIN_CONFIG_FILE_NAME;
|
|
8011
|
+
}
|
|
7428
8012
|
static getSettablePaths({ global = false } = {}) {
|
|
7429
8013
|
if (global) return {
|
|
7430
|
-
relativeDirPath:
|
|
7431
|
-
relativeFilePath:
|
|
8014
|
+
relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
|
|
8015
|
+
relativeFilePath: DEVIN_CONFIG_FILE_NAME
|
|
7432
8016
|
};
|
|
7433
8017
|
return {
|
|
7434
|
-
relativeDirPath:
|
|
7435
|
-
relativeFilePath:
|
|
8018
|
+
relativeDirPath: DEVIN_DIR,
|
|
8019
|
+
relativeFilePath: DEVIN_HOOKS_V1_FILE_NAME
|
|
7436
8020
|
};
|
|
7437
8021
|
}
|
|
7438
8022
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
7439
8023
|
const paths = DevinHooks.getSettablePaths({ global });
|
|
7440
|
-
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ??
|
|
8024
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
|
|
7441
8025
|
return new DevinHooks({
|
|
7442
8026
|
outputRoot,
|
|
7443
8027
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -7448,31 +8032,32 @@ var DevinHooks = class DevinHooks extends ToolHooks {
|
|
|
7448
8032
|
}
|
|
7449
8033
|
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
7450
8034
|
const paths = DevinHooks.getSettablePaths({ global });
|
|
7451
|
-
|
|
8035
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
7452
8036
|
const config = rulesyncHooks.getJson();
|
|
7453
|
-
const
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
}
|
|
7468
|
-
const objects = [];
|
|
7469
|
-
for (const def of defs) {
|
|
7470
|
-
const hook = canonicalDefToDevinHook(def);
|
|
7471
|
-
if (hook !== null) objects.push(hook);
|
|
8037
|
+
const devinHooks = canonicalToToolHooks({
|
|
8038
|
+
config,
|
|
8039
|
+
toolOverrideHooks: config.devin?.hooks,
|
|
8040
|
+
converterConfig: DEVIN_CONVERTER_CONFIG,
|
|
8041
|
+
logger
|
|
8042
|
+
});
|
|
8043
|
+
let fileContent;
|
|
8044
|
+
if (global) {
|
|
8045
|
+
const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
|
|
8046
|
+
let parsedSettings;
|
|
8047
|
+
try {
|
|
8048
|
+
parsedSettings = JSON.parse(existingContent);
|
|
8049
|
+
} catch (error) {
|
|
8050
|
+
throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
7472
8051
|
}
|
|
7473
|
-
|
|
8052
|
+
const merged = {
|
|
8053
|
+
...isRecord(parsedSettings) ? parsedSettings : {},
|
|
8054
|
+
hooks: devinHooks
|
|
8055
|
+
};
|
|
8056
|
+
fileContent = JSON.stringify(merged, null, 2);
|
|
8057
|
+
} else {
|
|
8058
|
+
await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
|
|
8059
|
+
fileContent = JSON.stringify(devinHooks, null, 2);
|
|
7474
8060
|
}
|
|
7475
|
-
const fileContent = JSON.stringify({ hooks: devinHooks }, null, 2);
|
|
7476
8061
|
return new DevinHooks({
|
|
7477
8062
|
outputRoot,
|
|
7478
8063
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -7488,22 +8073,13 @@ var DevinHooks = class DevinHooks extends ToolHooks {
|
|
|
7488
8073
|
} catch (error) {
|
|
7489
8074
|
throw new Error(`Failed to parse Devin hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
7490
8075
|
}
|
|
7491
|
-
const
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
const canonicalEvent = DEVIN_TO_CANONICAL_EVENT_NAMES[devinEvent];
|
|
7496
|
-
if (canonicalEvent === void 0) continue;
|
|
7497
|
-
const defs = [];
|
|
7498
|
-
for (const rawObject of rawObjects) {
|
|
7499
|
-
if (!isRecord(rawObject)) continue;
|
|
7500
|
-
defs.push(devinHookToCanonicalDef(rawObject));
|
|
7501
|
-
}
|
|
7502
|
-
if (defs.length > 0) canonicalHooks[canonicalEvent] = defs;
|
|
7503
|
-
}
|
|
8076
|
+
const hooks = toolHooksToCanonical({
|
|
8077
|
+
hooks: this.getRelativeFilePath() === "config.json" ? isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {} : parsed,
|
|
8078
|
+
converterConfig: DEVIN_CONVERTER_CONFIG
|
|
8079
|
+
});
|
|
7504
8080
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
|
|
7505
8081
|
version: 1,
|
|
7506
|
-
hooks
|
|
8082
|
+
hooks
|
|
7507
8083
|
}, null, 2) });
|
|
7508
8084
|
}
|
|
7509
8085
|
validate() {
|
|
@@ -7517,7 +8093,7 @@ var DevinHooks = class DevinHooks extends ToolHooks {
|
|
|
7517
8093
|
outputRoot,
|
|
7518
8094
|
relativeDirPath,
|
|
7519
8095
|
relativeFilePath,
|
|
7520
|
-
fileContent:
|
|
8096
|
+
fileContent: "{}",
|
|
7521
8097
|
validate: false
|
|
7522
8098
|
});
|
|
7523
8099
|
}
|
|
@@ -7744,6 +8320,106 @@ function mergeHermesConfig(fileContent, patch) {
|
|
|
7744
8320
|
}
|
|
7745
8321
|
//#endregion
|
|
7746
8322
|
//#region src/features/hooks/hermesagent-hooks.ts
|
|
8323
|
+
/**
|
|
8324
|
+
* Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
|
|
8325
|
+
* `post_tool_call`) and therefore carry a `matcher`. Every other supported
|
|
8326
|
+
* canonical event maps to a Hermes lifecycle event, which never accepts a
|
|
8327
|
+
* `matcher`.
|
|
8328
|
+
* @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
|
|
8329
|
+
*/
|
|
8330
|
+
const HERMESAGENT_MATCHER_EVENTS = /* @__PURE__ */ new Set(["preToolUse", "postToolUse"]);
|
|
8331
|
+
/**
|
|
8332
|
+
* Filter the shared canonical hooks to the events Hermes understands and merge
|
|
8333
|
+
* the `hermesagent`-specific override block on top.
|
|
8334
|
+
*/
|
|
8335
|
+
function buildEffectiveHooks(config, toolOverrideHooks) {
|
|
8336
|
+
const supported = new Set(HERMESAGENT_HOOK_EVENTS);
|
|
8337
|
+
const shared = {};
|
|
8338
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (supported.has(event)) shared[event] = defs;
|
|
8339
|
+
return {
|
|
8340
|
+
...shared,
|
|
8341
|
+
...toolOverrideHooks
|
|
8342
|
+
};
|
|
8343
|
+
}
|
|
8344
|
+
/**
|
|
8345
|
+
* Convert the canonical hooks config into Hermes's native
|
|
8346
|
+
* `hooks: { <event>: [{ matcher?, command, timeout? }] }` shape.
|
|
8347
|
+
*
|
|
8348
|
+
* Only `type: "command"` canonical hooks are emitted — Hermes shell hooks run
|
|
8349
|
+
* via `shlex.split`/`shell=False`, so `prompt`/`http` hooks have no native
|
|
8350
|
+
* equivalent and are skipped (the shared `HooksProcessor` already warns about
|
|
8351
|
+
* unsupported hook types centrally). `matcher` is only carried through for
|
|
8352
|
+
* `pre_tool_call`/`post_tool_call`; on any other event it is dropped with a
|
|
8353
|
+
* warning, mirroring how other adapters (e.g. AugmentCode) handle
|
|
8354
|
+
* matcher-less lifecycle events.
|
|
8355
|
+
*/
|
|
8356
|
+
function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
|
|
8357
|
+
const effectiveHooks = buildEffectiveHooks(config, toolOverrideHooks);
|
|
8358
|
+
const result = {};
|
|
8359
|
+
for (const [canonicalEvent, defs] of Object.entries(effectiveHooks)) {
|
|
8360
|
+
const nativeEvent = CANONICAL_TO_HERMESAGENT_EVENT_NAMES[canonicalEvent];
|
|
8361
|
+
if (!nativeEvent) continue;
|
|
8362
|
+
const supportsMatcher = HERMESAGENT_MATCHER_EVENTS.has(canonicalEvent);
|
|
8363
|
+
const entries = [];
|
|
8364
|
+
for (const def of defs) {
|
|
8365
|
+
if ((def.type ?? "command") !== "command") continue;
|
|
8366
|
+
if (typeof def.command !== "string" || def.command === "") continue;
|
|
8367
|
+
const entry = { command: def.command };
|
|
8368
|
+
if (typeof def.matcher === "string" && def.matcher !== "") if (supportsMatcher) entry.matcher = def.matcher;
|
|
8369
|
+
else logger?.warn(`matcher "${def.matcher}" on "${canonicalEvent}" hook will be ignored — Hermes Agent only supports matchers on pre_tool_call/post_tool_call`);
|
|
8370
|
+
if (typeof def.timeout === "number") entry.timeout = def.timeout;
|
|
8371
|
+
entries.push(entry);
|
|
8372
|
+
}
|
|
8373
|
+
if (entries.length > 0) result[nativeEvent] = entries;
|
|
8374
|
+
}
|
|
8375
|
+
return result;
|
|
8376
|
+
}
|
|
8377
|
+
/**
|
|
8378
|
+
* Reverse {@link canonicalToHermesHooks}: parse Hermes's native
|
|
8379
|
+
* `hooks: { <event>: [...] }` map back into a canonical event → definition[]
|
|
8380
|
+
* record. Native events with no canonical equivalent (`pre_verify`,
|
|
8381
|
+
* `transform_tool_result`, ...) are skipped since there is nothing to round
|
|
8382
|
+
* -trip them into.
|
|
8383
|
+
*/
|
|
8384
|
+
function hermesHooksToCanonical(hooks) {
|
|
8385
|
+
const canonical = {};
|
|
8386
|
+
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
|
|
8387
|
+
for (const [nativeEvent, entries] of Object.entries(hooks)) {
|
|
8388
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(nativeEvent) || !Array.isArray(entries)) continue;
|
|
8389
|
+
const canonicalEvent = HERMESAGENT_TO_CANONICAL_EVENT_NAMES[nativeEvent];
|
|
8390
|
+
if (!canonicalEvent) continue;
|
|
8391
|
+
const defs = [];
|
|
8392
|
+
for (const raw of entries) {
|
|
8393
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
8394
|
+
const entry = raw;
|
|
8395
|
+
if (typeof entry.command !== "string") continue;
|
|
8396
|
+
const def = {
|
|
8397
|
+
type: "command",
|
|
8398
|
+
command: entry.command
|
|
8399
|
+
};
|
|
8400
|
+
if (typeof entry.matcher === "string" && entry.matcher !== "") def.matcher = entry.matcher;
|
|
8401
|
+
if (typeof entry.timeout === "number") def.timeout = entry.timeout;
|
|
8402
|
+
defs.push(def);
|
|
8403
|
+
}
|
|
8404
|
+
if (defs.length > 0) canonical[canonicalEvent] = defs;
|
|
8405
|
+
}
|
|
8406
|
+
return canonical;
|
|
8407
|
+
}
|
|
8408
|
+
/**
|
|
8409
|
+
* Hermes Agent shell hooks.
|
|
8410
|
+
*
|
|
8411
|
+
* Hermes Agent registers shell-command hooks under the `hooks:` key of the
|
|
8412
|
+
* shared user config file `~/.hermes/config.yaml` (the HERMES_HOME directory;
|
|
8413
|
+
* global only — Hermes has no project-scoped hooks location). Hermes only runs
|
|
8414
|
+
* hooks declared under its fixed `VALID_HOOKS` event keys (`pre_tool_call`,
|
|
8415
|
+
* `post_tool_call`, `pre_llm_call`, `post_llm_call`, `on_session_start`,
|
|
8416
|
+
* `on_session_end`, `subagent_start`, `subagent_stop`, ...); any other key is
|
|
8417
|
+
* silently ignored. Generation therefore maps canonical events onto the real
|
|
8418
|
+
* `VALID_HOOKS` keys and merges the resulting `hooks:` block into the existing
|
|
8419
|
+
* config instead of overwriting it, since that file also holds other Hermes
|
|
8420
|
+
* settings (model, `mcp_servers`, `command_allowlist`, ...).
|
|
8421
|
+
* @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
|
|
8422
|
+
*/
|
|
7747
8423
|
var HermesagentHooks = class HermesagentHooks extends ToolHooks {
|
|
7748
8424
|
static getSettablePaths() {
|
|
7749
8425
|
return {
|
|
@@ -7763,6 +8439,9 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
|
|
|
7763
8439
|
error: null
|
|
7764
8440
|
};
|
|
7765
8441
|
}
|
|
8442
|
+
isDeletable() {
|
|
8443
|
+
return false;
|
|
8444
|
+
}
|
|
7766
8445
|
shouldMergeExistingFileContent() {
|
|
7767
8446
|
return true;
|
|
7768
8447
|
}
|
|
@@ -7770,18 +8449,21 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
|
|
|
7770
8449
|
this.fileContent = mergeHermesConfig(fileContent, parseHermesConfig(this.fileContent));
|
|
7771
8450
|
}
|
|
7772
8451
|
toRulesyncHooks() {
|
|
7773
|
-
const
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
fileContent: JSON.stringify(hooks ?? {}, null, 2)
|
|
7779
|
-
});
|
|
8452
|
+
const hooks = hermesHooksToCanonical(parseHermesConfig(this.getFileContent()).hooks);
|
|
8453
|
+
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
|
|
8454
|
+
version: 1,
|
|
8455
|
+
hooks
|
|
8456
|
+
}, null, 2) });
|
|
7780
8457
|
}
|
|
7781
|
-
static fromRulesyncHooks({ outputRoot, rulesyncHooks }) {
|
|
8458
|
+
static fromRulesyncHooks({ outputRoot, rulesyncHooks, logger }) {
|
|
8459
|
+
const config = rulesyncHooks.getJson();
|
|
7782
8460
|
return new HermesagentHooks({
|
|
7783
8461
|
outputRoot,
|
|
7784
|
-
fileContent: stringifyHermesConfig({ hooks: {
|
|
8462
|
+
fileContent: stringifyHermesConfig({ hooks: canonicalToHermesHooks({
|
|
8463
|
+
config,
|
|
8464
|
+
toolOverrideHooks: config.hermesagent?.hooks,
|
|
8465
|
+
logger
|
|
8466
|
+
}) })
|
|
7785
8467
|
});
|
|
7786
8468
|
}
|
|
7787
8469
|
};
|
|
@@ -8251,9 +8933,8 @@ var KiroHooks = class KiroHooks extends ToolHooks {
|
|
|
8251
8933
|
* the tool-specific override key to `kiro-cli` (so `kiro-cli.hooks` overrides in
|
|
8252
8934
|
* the rulesync hooks config are honored, rather than the legacy `kiro.hooks`).
|
|
8253
8935
|
*
|
|
8254
|
-
* (The Kiro IDE uses
|
|
8255
|
-
*
|
|
8256
|
-
* adapter.)
|
|
8936
|
+
* (The Kiro IDE uses the structured `.kiro/hooks/*.json` v1 format instead; see
|
|
8937
|
+
* {@link import("./kiro-ide-hooks.js").KiroIdeHooks}.)
|
|
8257
8938
|
*/
|
|
8258
8939
|
var KiroCliHooks = class extends KiroHooks {
|
|
8259
8940
|
static getOverrideKey() {
|
|
@@ -8261,6 +8942,203 @@ var KiroCliHooks = class extends KiroHooks {
|
|
|
8261
8942
|
}
|
|
8262
8943
|
};
|
|
8263
8944
|
//#endregion
|
|
8945
|
+
//#region src/features/hooks/kiro-ide-hooks.ts
|
|
8946
|
+
/**
|
|
8947
|
+
* One hook entry inside the Kiro IDE v1 `hooks` array.
|
|
8948
|
+
*
|
|
8949
|
+
* `z.looseObject` keeps unknown fields added by future Kiro IDE versions, so
|
|
8950
|
+
* imports do not drop data they do not yet understand.
|
|
8951
|
+
* @see https://kiro.dev/docs/hooks/types/
|
|
8952
|
+
*/
|
|
8953
|
+
const KiroIdeHookActionSchema = zod_mini.z.union([zod_mini.z.looseObject({
|
|
8954
|
+
type: zod_mini.z.literal("command"),
|
|
8955
|
+
command: zod_mini.z.optional(safeString)
|
|
8956
|
+
}), zod_mini.z.looseObject({
|
|
8957
|
+
type: zod_mini.z.literal("agent"),
|
|
8958
|
+
prompt: zod_mini.z.optional(safeString)
|
|
8959
|
+
})]);
|
|
8960
|
+
const KiroIdeHookEntrySchema = zod_mini.z.looseObject({
|
|
8961
|
+
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
8962
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
8963
|
+
trigger: zod_mini.z.optional(zod_mini.z.string()),
|
|
8964
|
+
matcher: zod_mini.z.optional(zod_mini.z.string()),
|
|
8965
|
+
action: zod_mini.z.optional(KiroIdeHookActionSchema),
|
|
8966
|
+
timeout: zod_mini.z.optional(zod_mini.z.number()),
|
|
8967
|
+
enabled: zod_mini.z.optional(zod_mini.z.boolean())
|
|
8968
|
+
});
|
|
8969
|
+
const KiroIdeHooksFileSchema = zod_mini.z.looseObject({
|
|
8970
|
+
version: zod_mini.z.optional(zod_mini.z.string()),
|
|
8971
|
+
hooks: zod_mini.z.optional(zod_mini.z.array(KiroIdeHookEntrySchema))
|
|
8972
|
+
});
|
|
8973
|
+
/**
|
|
8974
|
+
* Build the Kiro IDE hook entries for a single canonical event's definitions.
|
|
8975
|
+
*
|
|
8976
|
+
* `command`-type definitions become `{ type: "command", command }` actions and
|
|
8977
|
+
* `prompt`-type definitions become `{ type: "agent", prompt }` actions. Other
|
|
8978
|
+
* types are skipped (the {@link import("./hooks-processor.js").HooksProcessor}
|
|
8979
|
+
* already warns about unsupported types).
|
|
8980
|
+
*/
|
|
8981
|
+
function buildKiroIdeEntriesForEvent(trigger, definitions) {
|
|
8982
|
+
const entries = [];
|
|
8983
|
+
for (const def of definitions) {
|
|
8984
|
+
const type = def.type ?? "command";
|
|
8985
|
+
let action;
|
|
8986
|
+
if (type === "command") {
|
|
8987
|
+
if (def.command === void 0) continue;
|
|
8988
|
+
action = {
|
|
8989
|
+
type: "command",
|
|
8990
|
+
command: def.command
|
|
8991
|
+
};
|
|
8992
|
+
} else if (type === "prompt") {
|
|
8993
|
+
if (def.prompt === void 0) continue;
|
|
8994
|
+
action = {
|
|
8995
|
+
type: "agent",
|
|
8996
|
+
prompt: def.prompt
|
|
8997
|
+
};
|
|
8998
|
+
} else continue;
|
|
8999
|
+
entries.push({
|
|
9000
|
+
name: def.name ?? trigger,
|
|
9001
|
+
...def.description !== void 0 && def.description !== null && { description: def.description },
|
|
9002
|
+
trigger,
|
|
9003
|
+
...def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" && { matcher: def.matcher },
|
|
9004
|
+
action,
|
|
9005
|
+
...def.timeout !== void 0 && def.timeout !== null && def.timeout >= 0 && { timeout: def.timeout },
|
|
9006
|
+
enabled: true
|
|
9007
|
+
});
|
|
9008
|
+
}
|
|
9009
|
+
return entries;
|
|
9010
|
+
}
|
|
9011
|
+
function canonicalToKiroIdeHooks(config) {
|
|
9012
|
+
const kiroIdeSupported = new Set(KIRO_IDE_HOOK_EVENTS);
|
|
9013
|
+
const sharedHooks = {};
|
|
9014
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (kiroIdeSupported.has(event)) sharedHooks[event] = defs;
|
|
9015
|
+
const effectiveHooks = {
|
|
9016
|
+
...sharedHooks,
|
|
9017
|
+
...config["kiro-ide"]?.hooks
|
|
9018
|
+
};
|
|
9019
|
+
const entries = [];
|
|
9020
|
+
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
9021
|
+
const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? eventName;
|
|
9022
|
+
entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
|
|
9023
|
+
}
|
|
9024
|
+
return entries;
|
|
9025
|
+
}
|
|
9026
|
+
function kiroIdeHooksToCanonical(entries) {
|
|
9027
|
+
const canonical = {};
|
|
9028
|
+
for (const entry of entries) {
|
|
9029
|
+
if (entry.trigger === void 0 || entry.action === void 0) continue;
|
|
9030
|
+
const eventName = KIRO_IDE_TO_CANONICAL_EVENT_NAMES[entry.trigger] ?? entry.trigger;
|
|
9031
|
+
if (isPrototypePollutionKey(eventName)) continue;
|
|
9032
|
+
const def = {};
|
|
9033
|
+
if (entry.action.type === "command") {
|
|
9034
|
+
if (!entry.action.command) continue;
|
|
9035
|
+
def.type = "command";
|
|
9036
|
+
def.command = entry.action.command;
|
|
9037
|
+
} else {
|
|
9038
|
+
if (!entry.action.prompt) continue;
|
|
9039
|
+
def.type = "prompt";
|
|
9040
|
+
def.prompt = entry.action.prompt;
|
|
9041
|
+
}
|
|
9042
|
+
if (entry.name !== void 0 && entry.name !== null) def.name = entry.name;
|
|
9043
|
+
if (entry.description !== void 0 && entry.description !== null) def.description = entry.description;
|
|
9044
|
+
if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
|
|
9045
|
+
if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
|
|
9046
|
+
(canonical[eventName] ??= []).push(def);
|
|
9047
|
+
}
|
|
9048
|
+
return canonical;
|
|
9049
|
+
}
|
|
9050
|
+
/**
|
|
9051
|
+
* Hooks generator for the **Kiro IDE** (`.kiro/hooks/*.json` v1).
|
|
9052
|
+
*
|
|
9053
|
+
* Kiro IDE 1.0 reads structured JSON hooks from `.kiro/hooks/` (workspace) and
|
|
9054
|
+
* `~/.kiro/hooks/` (user). A single file may declare multiple hooks in its
|
|
9055
|
+
* `hooks` array, so rulesync emits every generated hook into one
|
|
9056
|
+
* `rulesync.json` file per scope (`{ "version": "v1", "hooks": [ ... ] }`),
|
|
9057
|
+
* which keeps it within the single-file hooks architecture.
|
|
9058
|
+
*
|
|
9059
|
+
* This is distinct from the Kiro CLI ({@link import("./kiro-cli-hooks.js").
|
|
9060
|
+
* KiroCliHooks}), which uses the `.kiro/agents/default.json` agent-config shape.
|
|
9061
|
+
*
|
|
9062
|
+
* @see https://kiro.dev/docs/hooks/
|
|
9063
|
+
*/
|
|
9064
|
+
var KiroIdeHooks = class KiroIdeHooks extends ToolHooks {
|
|
9065
|
+
constructor(params) {
|
|
9066
|
+
super({
|
|
9067
|
+
...params,
|
|
9068
|
+
fileContent: params.fileContent ?? JSON.stringify({
|
|
9069
|
+
version: "v1",
|
|
9070
|
+
hooks: []
|
|
9071
|
+
}, null, 2)
|
|
9072
|
+
});
|
|
9073
|
+
}
|
|
9074
|
+
static getSettablePaths(_options = {}) {
|
|
9075
|
+
return {
|
|
9076
|
+
relativeDirPath: KIRO_IDE_HOOKS_DIR_PATH,
|
|
9077
|
+
relativeFilePath: KIRO_IDE_HOOKS_FILE_NAME
|
|
9078
|
+
};
|
|
9079
|
+
}
|
|
9080
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
9081
|
+
const paths = KiroIdeHooks.getSettablePaths({ global });
|
|
9082
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({
|
|
9083
|
+
version: "v1",
|
|
9084
|
+
hooks: []
|
|
9085
|
+
}, null, 2);
|
|
9086
|
+
return new KiroIdeHooks({
|
|
9087
|
+
outputRoot,
|
|
9088
|
+
relativeDirPath: paths.relativeDirPath,
|
|
9089
|
+
relativeFilePath: paths.relativeFilePath,
|
|
9090
|
+
fileContent,
|
|
9091
|
+
validate
|
|
9092
|
+
});
|
|
9093
|
+
}
|
|
9094
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
|
|
9095
|
+
const paths = KiroIdeHooks.getSettablePaths({ global });
|
|
9096
|
+
const hooks = canonicalToKiroIdeHooks(rulesyncHooks.getJson());
|
|
9097
|
+
const fileContent = JSON.stringify({
|
|
9098
|
+
version: "v1",
|
|
9099
|
+
hooks
|
|
9100
|
+
}, null, 2);
|
|
9101
|
+
return new KiroIdeHooks({
|
|
9102
|
+
outputRoot,
|
|
9103
|
+
relativeDirPath: paths.relativeDirPath,
|
|
9104
|
+
relativeFilePath: paths.relativeFilePath,
|
|
9105
|
+
fileContent,
|
|
9106
|
+
validate
|
|
9107
|
+
});
|
|
9108
|
+
}
|
|
9109
|
+
toRulesyncHooks() {
|
|
9110
|
+
let parsed;
|
|
9111
|
+
try {
|
|
9112
|
+
parsed = KiroIdeHooksFileSchema.parse(JSON.parse(this.getFileContent()));
|
|
9113
|
+
} catch (error) {
|
|
9114
|
+
throw new Error(`Failed to parse Kiro IDE hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
9115
|
+
}
|
|
9116
|
+
const hooks = kiroIdeHooksToCanonical(parsed.hooks ?? []);
|
|
9117
|
+
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
|
|
9118
|
+
version: 1,
|
|
9119
|
+
hooks
|
|
9120
|
+
}, null, 2) });
|
|
9121
|
+
}
|
|
9122
|
+
validate() {
|
|
9123
|
+
return {
|
|
9124
|
+
success: true,
|
|
9125
|
+
error: null
|
|
9126
|
+
};
|
|
9127
|
+
}
|
|
9128
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
9129
|
+
return new KiroIdeHooks({
|
|
9130
|
+
outputRoot,
|
|
9131
|
+
relativeDirPath,
|
|
9132
|
+
relativeFilePath,
|
|
9133
|
+
fileContent: JSON.stringify({
|
|
9134
|
+
version: "v1",
|
|
9135
|
+
hooks: []
|
|
9136
|
+
}, null, 2),
|
|
9137
|
+
validate: false
|
|
9138
|
+
});
|
|
9139
|
+
}
|
|
9140
|
+
};
|
|
9141
|
+
//#endregion
|
|
8264
9142
|
//#region src/features/hooks/opencode-hooks.ts
|
|
8265
9143
|
var OpencodeHooks = class OpencodeHooks extends ToolHooks {
|
|
8266
9144
|
constructor(params) {
|
|
@@ -8319,6 +9197,35 @@ var OpencodeHooks = class OpencodeHooks extends ToolHooks {
|
|
|
8319
9197
|
//#endregion
|
|
8320
9198
|
//#region src/features/hooks/qwencode-hooks.ts
|
|
8321
9199
|
/**
|
|
9200
|
+
* Build a single Qwen Code hook object from a canonical hook definition.
|
|
9201
|
+
* Command-only fields (`async`/`env`/`shell`) are emitted only on command hooks
|
|
9202
|
+
* and http-only fields (`headers`/`allowedEnvVars`/`once`) only on http hooks,
|
|
9203
|
+
* matching upstream. `statusMessage` applies to both.
|
|
9204
|
+
* https://github.com/QwenLM/qwen-code/blob/main/docs/users/features/hooks.md
|
|
9205
|
+
*/
|
|
9206
|
+
function canonicalDefToQwencodeHook(def) {
|
|
9207
|
+
const type = def.type ?? "command";
|
|
9208
|
+
const isHttp = type === "http";
|
|
9209
|
+
const isCommand = type === "command";
|
|
9210
|
+
return {
|
|
9211
|
+
type,
|
|
9212
|
+
...compact({
|
|
9213
|
+
command: def.command,
|
|
9214
|
+
url: def.url,
|
|
9215
|
+
timeout: def.timeout,
|
|
9216
|
+
name: def.name,
|
|
9217
|
+
description: def.description,
|
|
9218
|
+
statusMessage: def.statusMessage,
|
|
9219
|
+
async: isCommand ? def.async : void 0,
|
|
9220
|
+
env: isCommand ? def.env : void 0,
|
|
9221
|
+
shell: isCommand ? def.shell : void 0,
|
|
9222
|
+
headers: isHttp ? def.headers : void 0,
|
|
9223
|
+
allowedEnvVars: isHttp ? def.allowedEnvVars : void 0,
|
|
9224
|
+
once: isHttp ? def.once : void 0
|
|
9225
|
+
})
|
|
9226
|
+
};
|
|
9227
|
+
}
|
|
9228
|
+
/**
|
|
8322
9229
|
* Convert canonical hooks config to Qwen Code format.
|
|
8323
9230
|
* Filters shared hooks to QWENCODE_HOOK_EVENTS, merges config.qwencode?.hooks,
|
|
8324
9231
|
* then converts to PascalCase and Qwen Code matcher/hooks structure.
|
|
@@ -8346,16 +9253,7 @@ function canonicalToQwencodeHooks(config) {
|
|
|
8346
9253
|
}
|
|
8347
9254
|
const entries = [];
|
|
8348
9255
|
for (const [matcherKey, defs] of byMatcher) {
|
|
8349
|
-
const hooks = defs.map(
|
|
8350
|
-
return {
|
|
8351
|
-
type: def.type ?? "command",
|
|
8352
|
-
...def.command !== void 0 && def.command !== null && { command: def.command },
|
|
8353
|
-
...def.url !== void 0 && def.url !== null && { url: def.url },
|
|
8354
|
-
...def.timeout !== void 0 && def.timeout !== null && { timeout: def.timeout },
|
|
8355
|
-
...def.name !== void 0 && def.name !== null && { name: def.name },
|
|
8356
|
-
...def.description !== void 0 && def.description !== null && { description: def.description }
|
|
8357
|
-
};
|
|
8358
|
-
});
|
|
9256
|
+
const hooks = defs.map(canonicalDefToQwencodeHook);
|
|
8359
9257
|
const sequential = defs.some((def) => def.sequential === true);
|
|
8360
9258
|
const group = matcherKey ? {
|
|
8361
9259
|
matcher: matcherKey,
|
|
@@ -8379,7 +9277,14 @@ const QwencodeHookEntrySchema = zod_mini.z.looseObject({
|
|
|
8379
9277
|
url: zod_mini.z.optional(zod_mini.z.string()),
|
|
8380
9278
|
timeout: zod_mini.z.optional(zod_mini.z.number()),
|
|
8381
9279
|
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
8382
|
-
description: zod_mini.z.optional(zod_mini.z.string())
|
|
9280
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
9281
|
+
statusMessage: zod_mini.z.optional(zod_mini.z.string()),
|
|
9282
|
+
async: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
9283
|
+
env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
9284
|
+
shell: zod_mini.z.optional(zod_mini.z.string()),
|
|
9285
|
+
headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
9286
|
+
allowedEnvVars: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
9287
|
+
once: zod_mini.z.optional(zod_mini.z.boolean())
|
|
8383
9288
|
});
|
|
8384
9289
|
/**
|
|
8385
9290
|
* A matcher group entry in a Qwen Code event array.
|
|
@@ -8398,18 +9303,29 @@ function qwencodeMatcherEntryToCanonical(entry) {
|
|
|
8398
9303
|
const defs = [];
|
|
8399
9304
|
const hooks = entry.hooks ?? [];
|
|
8400
9305
|
const sequential = entry.sequential === true;
|
|
9306
|
+
const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" ? entry.matcher : void 0;
|
|
8401
9307
|
for (const h of hooks) {
|
|
8402
|
-
const command = h.command;
|
|
8403
9308
|
const hookType = h.type === "command" || h.type === "prompt" || h.type === "http" ? h.type : "command";
|
|
9309
|
+
const isHttp = hookType === "http";
|
|
9310
|
+
const isCommand = hookType === "command";
|
|
8404
9311
|
defs.push({
|
|
8405
9312
|
type: hookType,
|
|
8406
|
-
...
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
9313
|
+
...compact({
|
|
9314
|
+
command: h.command,
|
|
9315
|
+
url: h.url,
|
|
9316
|
+
timeout: h.timeout,
|
|
9317
|
+
name: h.name,
|
|
9318
|
+
description: h.description,
|
|
9319
|
+
statusMessage: h.statusMessage,
|
|
9320
|
+
async: isCommand ? h.async : void 0,
|
|
9321
|
+
env: isCommand ? h.env : void 0,
|
|
9322
|
+
shell: isCommand ? h.shell : void 0,
|
|
9323
|
+
headers: isHttp ? h.headers : void 0,
|
|
9324
|
+
allowedEnvVars: isHttp ? h.allowedEnvVars : void 0,
|
|
9325
|
+
once: isHttp ? h.once : void 0,
|
|
9326
|
+
sequential: sequential ? true : void 0,
|
|
9327
|
+
matcher
|
|
9328
|
+
})
|
|
8413
9329
|
});
|
|
8414
9330
|
}
|
|
8415
9331
|
return defs;
|
|
@@ -8522,6 +9438,169 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
|
|
|
8522
9438
|
}
|
|
8523
9439
|
};
|
|
8524
9440
|
//#endregion
|
|
9441
|
+
//#region src/features/hooks/reasonix-hooks.ts
|
|
9442
|
+
/**
|
|
9443
|
+
* Only PreToolUse/PostToolUse honor the `match` field (an anchored regex
|
|
9444
|
+
* against the tool name); it is ignored on every other event.
|
|
9445
|
+
*/
|
|
9446
|
+
const REASONIX_MATCHER_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse"]);
|
|
9447
|
+
const SUPPORTED_REASONIX_EVENTS = new Set(REASONIX_HOOK_EVENTS);
|
|
9448
|
+
/**
|
|
9449
|
+
* Convert canonical hooks config to the Reasonix `hooks` object.
|
|
9450
|
+
* Filters shared hooks to REASONIX_HOOK_EVENTS, merges `config.reasonix?.hooks`,
|
|
9451
|
+
* then maps event names and emits flat per-event hook-entry arrays.
|
|
9452
|
+
*/
|
|
9453
|
+
function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
|
|
9454
|
+
const sharedHooks = {};
|
|
9455
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_REASONIX_EVENTS.has(event)) sharedHooks[event] = defs;
|
|
9456
|
+
const effectiveHooks = {
|
|
9457
|
+
...sharedHooks,
|
|
9458
|
+
...toolOverrideHooks
|
|
9459
|
+
};
|
|
9460
|
+
const result = {};
|
|
9461
|
+
for (const [event, defs] of Object.entries(effectiveHooks)) {
|
|
9462
|
+
if (!SUPPORTED_REASONIX_EVENTS.has(event)) continue;
|
|
9463
|
+
const reasonixEvent = CANONICAL_TO_REASONIX_EVENT_NAMES[event] ?? event;
|
|
9464
|
+
const isMatcherEvent = REASONIX_MATCHER_EVENTS.has(reasonixEvent);
|
|
9465
|
+
const entries = [];
|
|
9466
|
+
for (const def of defs) {
|
|
9467
|
+
if ((def.type ?? "command") !== "command") continue;
|
|
9468
|
+
if (typeof def.command !== "string") continue;
|
|
9469
|
+
const entry = { command: def.command };
|
|
9470
|
+
if (typeof def.matcher === "string" && def.matcher !== "") if (isMatcherEvent) entry.match = def.matcher;
|
|
9471
|
+
else logger?.warn(`matcher "${def.matcher}" on "${event}" hook will be ignored — Reasonix's "${reasonixEvent}" event does not support matchers`);
|
|
9472
|
+
if (typeof def.description === "string" && def.description !== "") entry.description = def.description;
|
|
9473
|
+
if (typeof def.timeout === "number") entry.timeout = Math.round(def.timeout * 1e3);
|
|
9474
|
+
entries.push(entry);
|
|
9475
|
+
}
|
|
9476
|
+
if (entries.length > 0) result[reasonixEvent] = [...result[reasonixEvent] ?? [], ...entries];
|
|
9477
|
+
}
|
|
9478
|
+
return result;
|
|
9479
|
+
}
|
|
9480
|
+
/**
|
|
9481
|
+
* Reverse {@link canonicalToReasonixHooks}: parse the Reasonix `hooks` object
|
|
9482
|
+
* back into a canonical event -> definition[] record.
|
|
9483
|
+
*/
|
|
9484
|
+
function reasonixHooksToCanonical(hooks) {
|
|
9485
|
+
const canonical = {};
|
|
9486
|
+
if (hooks === null || hooks === void 0 || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
|
|
9487
|
+
for (const [reasonixEvent, rawEntries] of Object.entries(hooks)) {
|
|
9488
|
+
if (!Array.isArray(rawEntries)) continue;
|
|
9489
|
+
const canonicalEvent = REASONIX_TO_CANONICAL_EVENT_NAMES[reasonixEvent] ?? reasonixEvent;
|
|
9490
|
+
const defs = [];
|
|
9491
|
+
for (const rawEntry of rawEntries) {
|
|
9492
|
+
if (rawEntry === null || typeof rawEntry !== "object" || Array.isArray(rawEntry)) continue;
|
|
9493
|
+
const entry = rawEntry;
|
|
9494
|
+
if (typeof entry.command !== "string") continue;
|
|
9495
|
+
const def = {
|
|
9496
|
+
type: "command",
|
|
9497
|
+
command: entry.command
|
|
9498
|
+
};
|
|
9499
|
+
if (typeof entry.match === "string" && entry.match !== "") def.matcher = entry.match;
|
|
9500
|
+
if (typeof entry.description === "string" && entry.description !== "") def.description = entry.description;
|
|
9501
|
+
if (typeof entry.timeout === "number") def.timeout = entry.timeout / 1e3;
|
|
9502
|
+
defs.push(def);
|
|
9503
|
+
}
|
|
9504
|
+
if (defs.length > 0) canonical[canonicalEvent] = [...canonical[canonicalEvent] ?? [], ...defs];
|
|
9505
|
+
}
|
|
9506
|
+
return canonical;
|
|
9507
|
+
}
|
|
9508
|
+
/**
|
|
9509
|
+
* Reasonix hooks adapter.
|
|
9510
|
+
*
|
|
9511
|
+
* Reasonix hooks live in a Claude-Code-style but standalone JSON file —
|
|
9512
|
+
* `.reasonix/settings.json` (project) or `~/.reasonix/settings.json`
|
|
9513
|
+
* (global) — separate from the `[permissions]`/`[[plugins]]` TOML config.
|
|
9514
|
+
* Only the four events documented in the upstream issue are mapped:
|
|
9515
|
+
* PreToolUse/PostToolUse/UserPromptSubmit/Stop (see REASONIX_HOOK_EVENTS).
|
|
9516
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
|
|
9517
|
+
*/
|
|
9518
|
+
var ReasonixHooks = class ReasonixHooks extends ToolHooks {
|
|
9519
|
+
constructor(params) {
|
|
9520
|
+
super({
|
|
9521
|
+
...params,
|
|
9522
|
+
fileContent: params.fileContent ?? "{}"
|
|
9523
|
+
});
|
|
9524
|
+
}
|
|
9525
|
+
isDeletable() {
|
|
9526
|
+
return false;
|
|
9527
|
+
}
|
|
9528
|
+
static getSettablePaths(_options = {}) {
|
|
9529
|
+
return {
|
|
9530
|
+
relativeDirPath: REASONIX_DIR,
|
|
9531
|
+
relativeFilePath: REASONIX_SETTINGS_FILE_NAME
|
|
9532
|
+
};
|
|
9533
|
+
}
|
|
9534
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
9535
|
+
const paths = ReasonixHooks.getSettablePaths({ global });
|
|
9536
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
9537
|
+
return new ReasonixHooks({
|
|
9538
|
+
outputRoot,
|
|
9539
|
+
relativeDirPath: paths.relativeDirPath,
|
|
9540
|
+
relativeFilePath: paths.relativeFilePath,
|
|
9541
|
+
fileContent,
|
|
9542
|
+
validate
|
|
9543
|
+
});
|
|
9544
|
+
}
|
|
9545
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
9546
|
+
const paths = ReasonixHooks.getSettablePaths({ global });
|
|
9547
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
9548
|
+
const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
|
|
9549
|
+
let settings;
|
|
9550
|
+
try {
|
|
9551
|
+
settings = JSON.parse(existingContent);
|
|
9552
|
+
} catch (error) {
|
|
9553
|
+
throw new Error(`Failed to parse existing Reasonix settings at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
9554
|
+
}
|
|
9555
|
+
const config = rulesyncHooks.getJson();
|
|
9556
|
+
const reasonixHooks = canonicalToReasonixHooks({
|
|
9557
|
+
config,
|
|
9558
|
+
toolOverrideHooks: config.reasonix?.hooks,
|
|
9559
|
+
logger
|
|
9560
|
+
});
|
|
9561
|
+
const merged = {
|
|
9562
|
+
...settings,
|
|
9563
|
+
hooks: reasonixHooks
|
|
9564
|
+
};
|
|
9565
|
+
const fileContent = JSON.stringify(merged, null, 2);
|
|
9566
|
+
return new ReasonixHooks({
|
|
9567
|
+
outputRoot,
|
|
9568
|
+
relativeDirPath: paths.relativeDirPath,
|
|
9569
|
+
relativeFilePath: paths.relativeFilePath,
|
|
9570
|
+
fileContent,
|
|
9571
|
+
validate
|
|
9572
|
+
});
|
|
9573
|
+
}
|
|
9574
|
+
toRulesyncHooks() {
|
|
9575
|
+
let settings;
|
|
9576
|
+
try {
|
|
9577
|
+
settings = JSON.parse(this.getFileContent());
|
|
9578
|
+
} catch (error) {
|
|
9579
|
+
throw new Error(`Failed to parse Reasonix hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
9580
|
+
}
|
|
9581
|
+
const hooks = reasonixHooksToCanonical(settings.hooks);
|
|
9582
|
+
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
|
|
9583
|
+
version: 1,
|
|
9584
|
+
hooks
|
|
9585
|
+
}, null, 2) });
|
|
9586
|
+
}
|
|
9587
|
+
validate() {
|
|
9588
|
+
return {
|
|
9589
|
+
success: true,
|
|
9590
|
+
error: null
|
|
9591
|
+
};
|
|
9592
|
+
}
|
|
9593
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
9594
|
+
return new ReasonixHooks({
|
|
9595
|
+
outputRoot,
|
|
9596
|
+
relativeDirPath,
|
|
9597
|
+
relativeFilePath,
|
|
9598
|
+
fileContent: JSON.stringify({ hooks: {} }, null, 2),
|
|
9599
|
+
validate: false
|
|
9600
|
+
});
|
|
9601
|
+
}
|
|
9602
|
+
};
|
|
9603
|
+
//#endregion
|
|
8525
9604
|
//#region src/features/hooks/vibe-hooks.ts
|
|
8526
9605
|
const VIBE_DIR = ".vibe";
|
|
8527
9606
|
const VIBE_HOOKS_FILE_NAME = "hooks.toml";
|
|
@@ -8747,6 +9826,19 @@ var VibeHooks = class VibeHooks extends ToolHooks {
|
|
|
8747
9826
|
//#endregion
|
|
8748
9827
|
//#region src/features/hooks/hooks-processor.ts
|
|
8749
9828
|
const HooksProcessorToolTargetSchema = zod_mini.z.enum(hooksProcessorToolTargetTuple);
|
|
9829
|
+
/**
|
|
9830
|
+
* Event names present in the config that the target's adapter cannot emit.
|
|
9831
|
+
*
|
|
9832
|
+
* When the factory passes override-block keys through verbatim
|
|
9833
|
+
* (`passthroughOverrideEvents`), those keys are excluded from the check so
|
|
9834
|
+
* documented passthrough triggers aren't falsely reported as skipped.
|
|
9835
|
+
*/
|
|
9836
|
+
function unsupportedEventNames(params) {
|
|
9837
|
+
const { factory, sharedHooks, effectiveHooks } = params;
|
|
9838
|
+
const supportedEvents = new Set(factory.supportedEvents);
|
|
9839
|
+
const eventNames = factory.passthroughOverrideEvents ? Object.keys(sharedHooks) : Object.keys(effectiveHooks);
|
|
9840
|
+
return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
|
|
9841
|
+
}
|
|
8750
9842
|
const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
8751
9843
|
["antigravity-cli", {
|
|
8752
9844
|
class: AntigravityCliHooks,
|
|
@@ -8880,12 +9972,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
8880
9972
|
supportsGlobal: true,
|
|
8881
9973
|
supportsImport: true
|
|
8882
9974
|
},
|
|
8883
|
-
supportedEvents:
|
|
8884
|
-
supportedHookTypes: [
|
|
8885
|
-
"command",
|
|
8886
|
-
"prompt",
|
|
8887
|
-
"http"
|
|
8888
|
-
],
|
|
9975
|
+
supportedEvents: HERMESAGENT_HOOK_EVENTS,
|
|
9976
|
+
supportedHookTypes: ["command"],
|
|
8889
9977
|
supportsMatcher: true
|
|
8890
9978
|
}],
|
|
8891
9979
|
["deepagents", {
|
|
@@ -8921,6 +10009,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
8921
10009
|
supportedHookTypes: ["command"],
|
|
8922
10010
|
supportsMatcher: true
|
|
8923
10011
|
}],
|
|
10012
|
+
["kiro-ide", {
|
|
10013
|
+
class: KiroIdeHooks,
|
|
10014
|
+
meta: {
|
|
10015
|
+
supportsProject: true,
|
|
10016
|
+
supportsGlobal: true,
|
|
10017
|
+
supportsImport: true
|
|
10018
|
+
},
|
|
10019
|
+
supportedEvents: KIRO_IDE_HOOK_EVENTS,
|
|
10020
|
+
supportedHookTypes: ["command", "prompt"],
|
|
10021
|
+
supportsMatcher: true,
|
|
10022
|
+
passthroughOverrideEvents: true
|
|
10023
|
+
}],
|
|
8924
10024
|
["devin", {
|
|
8925
10025
|
class: DevinHooks,
|
|
8926
10026
|
meta: {
|
|
@@ -8929,8 +10029,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
8929
10029
|
supportsImport: true
|
|
8930
10030
|
},
|
|
8931
10031
|
supportedEvents: DEVIN_HOOK_EVENTS,
|
|
8932
|
-
supportedHookTypes: ["command"],
|
|
8933
|
-
supportsMatcher:
|
|
10032
|
+
supportedHookTypes: ["command", "prompt"],
|
|
10033
|
+
supportsMatcher: true
|
|
8934
10034
|
}],
|
|
8935
10035
|
["augmentcode", {
|
|
8936
10036
|
class: AugmentcodeHooks,
|
|
@@ -8975,6 +10075,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
8975
10075
|
supportedEvents: QWENCODE_HOOK_EVENTS,
|
|
8976
10076
|
supportedHookTypes: ["command"],
|
|
8977
10077
|
supportsMatcher: true
|
|
10078
|
+
}],
|
|
10079
|
+
["reasonix", {
|
|
10080
|
+
class: ReasonixHooks,
|
|
10081
|
+
meta: {
|
|
10082
|
+
supportsProject: true,
|
|
10083
|
+
supportsGlobal: true,
|
|
10084
|
+
supportsImport: true
|
|
10085
|
+
},
|
|
10086
|
+
supportedEvents: REASONIX_HOOK_EVENTS,
|
|
10087
|
+
supportedHookTypes: ["command"],
|
|
10088
|
+
supportsMatcher: true
|
|
8978
10089
|
}]
|
|
8979
10090
|
]);
|
|
8980
10091
|
const hooksProcessorToolTargets = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject).map(([t]) => t);
|
|
@@ -9050,8 +10161,11 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
9050
10161
|
...overrideHooks
|
|
9051
10162
|
};
|
|
9052
10163
|
{
|
|
9053
|
-
const
|
|
9054
|
-
|
|
10164
|
+
const skipped = unsupportedEventNames({
|
|
10165
|
+
factory,
|
|
10166
|
+
sharedHooks,
|
|
10167
|
+
effectiveHooks
|
|
10168
|
+
});
|
|
9055
10169
|
if (skipped.length > 0) this.logger.warn(`Skipped hook event(s) for ${this.toolTarget} (not supported): ${skipped.join(", ")}`);
|
|
9056
10170
|
}
|
|
9057
10171
|
{
|
|
@@ -9247,11 +10361,6 @@ var AiassistantIgnore = class AiassistantIgnore extends ToolIgnore {
|
|
|
9247
10361
|
}
|
|
9248
10362
|
};
|
|
9249
10363
|
//#endregion
|
|
9250
|
-
//#region src/constants/antigravity-cli-paths.ts
|
|
9251
|
-
const ANTIGRAVITY_AGENTS_DIR = ANTIGRAVITY_DIR;
|
|
9252
|
-
const ANTIGRAVITY_RULE_FILE_NAME = "AGENTS.md";
|
|
9253
|
-
const ANTIGRAVITY_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
|
|
9254
|
-
//#endregion
|
|
9255
10364
|
//#region src/features/ignore/antigravity-cli-ignore.ts
|
|
9256
10365
|
/**
|
|
9257
10366
|
* Antigravity CLI is the successor to Gemini CLI and is built on the same
|
|
@@ -10483,9 +11592,9 @@ function parseAmpSettingsJsonc(fileContent) {
|
|
|
10483
11592
|
}
|
|
10484
11593
|
function filterMcpServers(mcpServers) {
|
|
10485
11594
|
const filtered = {};
|
|
10486
|
-
if (!isRecord
|
|
11595
|
+
if (!isRecord(mcpServers)) return filtered;
|
|
10487
11596
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
10488
|
-
if (isPrototypePollutionKey(name) || !isRecord
|
|
11597
|
+
if (isPrototypePollutionKey(name) || !isRecord(config)) continue;
|
|
10489
11598
|
const filteredConfig = {};
|
|
10490
11599
|
for (const [key, value] of Object.entries(config)) {
|
|
10491
11600
|
if (isPrototypePollutionKey(key)) continue;
|
|
@@ -10600,7 +11709,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
10600
11709
|
success: true,
|
|
10601
11710
|
error: null
|
|
10602
11711
|
};
|
|
10603
|
-
if (!isRecord
|
|
11712
|
+
if (!isRecord(mcpServers)) return {
|
|
10604
11713
|
success: false,
|
|
10605
11714
|
error: /* @__PURE__ */ new Error(`${AMP_MCP_SERVERS_KEY} must be a JSON object`)
|
|
10606
11715
|
};
|
|
@@ -10609,7 +11718,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
10609
11718
|
success: false,
|
|
10610
11719
|
error: /* @__PURE__ */ new Error(`Server name "${serverName}" is a prototype pollution key and is not allowed`)
|
|
10611
11720
|
};
|
|
10612
|
-
if (!isRecord
|
|
11721
|
+
if (!isRecord(serverConfig)) return {
|
|
10613
11722
|
success: false,
|
|
10614
11723
|
error: /* @__PURE__ */ new Error(`MCP server "${serverName}" must be a JSON object`)
|
|
10615
11724
|
};
|
|
@@ -11130,7 +12239,7 @@ const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
|
|
|
11130
12239
|
function convertFromCodexFormat(codexMcp) {
|
|
11131
12240
|
const result = {};
|
|
11132
12241
|
for (const [name, config] of Object.entries(codexMcp)) {
|
|
11133
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
12242
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
11134
12243
|
const converted = {};
|
|
11135
12244
|
for (const [key, value] of Object.entries(config)) {
|
|
11136
12245
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
@@ -11150,7 +12259,7 @@ function convertToCodexFormat(mcpServers) {
|
|
|
11150
12259
|
const result = {};
|
|
11151
12260
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
11152
12261
|
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
11153
|
-
if (!isRecord
|
|
12262
|
+
if (!isRecord(config)) continue;
|
|
11154
12263
|
const converted = {};
|
|
11155
12264
|
for (const [key, value] of Object.entries(config)) {
|
|
11156
12265
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
@@ -11212,19 +12321,19 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
|
|
|
11212
12321
|
const strippedMcpServers = rulesyncMcp.getMcpServers();
|
|
11213
12322
|
const rawMcpServers = rulesyncMcp.getJson().mcpServers;
|
|
11214
12323
|
const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
|
|
11215
|
-
const rawServer = isRecord
|
|
12324
|
+
const rawServer = isRecord(rawMcpServers) ? rawMcpServers[serverName] : void 0;
|
|
11216
12325
|
return [serverName, {
|
|
11217
12326
|
...serverConfig,
|
|
11218
|
-
...isRecord
|
|
12327
|
+
...isRecord(rawServer) && isStringArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {}
|
|
11219
12328
|
}];
|
|
11220
12329
|
})));
|
|
11221
12330
|
const filteredMcpServers = this.removeEmptyEntries(converted);
|
|
11222
12331
|
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`);
|
|
11223
|
-
const existingMcpServers = isRecord
|
|
12332
|
+
const existingMcpServers = isRecord(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
|
|
11224
12333
|
configToml["mcp_servers"] = Object.fromEntries(Object.entries(filteredMcpServers).map(([name, serverConfig]) => {
|
|
11225
|
-
const existingServer = isRecord
|
|
12334
|
+
const existingServer = isRecord(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
|
|
11226
12335
|
const serverRecord = serverConfig;
|
|
11227
|
-
if (existingServer && isRecord
|
|
12336
|
+
if (existingServer && isRecord(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
|
|
11228
12337
|
...serverRecord,
|
|
11229
12338
|
tools: existingServer["tools"]
|
|
11230
12339
|
}];
|
|
@@ -11734,18 +12843,20 @@ var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
|
|
|
11734
12843
|
//#endregion
|
|
11735
12844
|
//#region src/features/mcp/devin-mcp.ts
|
|
11736
12845
|
/**
|
|
11737
|
-
* MCP generator for Devin (
|
|
12846
|
+
* MCP generator for Devin Local (native `.devin/` configuration).
|
|
11738
12847
|
*
|
|
11739
|
-
* Devin reads
|
|
11740
|
-
* - Project scope: `.
|
|
11741
|
-
* - Global scope: `~/.
|
|
12848
|
+
* Devin reads MCP servers from the `mcpServers` key of its native config file:
|
|
12849
|
+
* - Project scope: `.devin/config.json`
|
|
12850
|
+
* - Global scope: `~/.config/devin/config.json`
|
|
11742
12851
|
*
|
|
11743
|
-
* The
|
|
11744
|
-
* the
|
|
11745
|
-
*
|
|
11746
|
-
* stdio entry ({ command, args, env }) or a
|
|
11747
|
-
* ({ serverUrl | url, headers }), and may carry an optional
|
|
11748
|
-
* array.
|
|
12852
|
+
* The config file is shared with the permissions feature (`permissions` key)
|
|
12853
|
+
* and, in global mode, the hooks feature (`hooks` key), so reads and writes
|
|
12854
|
+
* merge into the existing JSON rather than overwriting it, and the file is
|
|
12855
|
+
* never deleted. Each server is a stdio entry ({ command, args, env }) or a
|
|
12856
|
+
* remote entry ({ serverUrl | url, headers }), and may carry an optional
|
|
12857
|
+
* `disabledTools` array.
|
|
12858
|
+
*
|
|
12859
|
+
* @see https://docs.devin.ai/cli/extensibility/configuration
|
|
11749
12860
|
*/
|
|
11750
12861
|
var DevinMcp = class DevinMcp extends ToolMcp {
|
|
11751
12862
|
json;
|
|
@@ -11763,14 +12874,21 @@ var DevinMcp = class DevinMcp extends ToolMcp {
|
|
|
11763
12874
|
throw new Error(`Failed to parse Devin MCP config at ${(0, node_path.join)(relativeDirPath, relativeFilePath)}: ${formatError(error)}`, { cause: error });
|
|
11764
12875
|
}
|
|
11765
12876
|
}
|
|
12877
|
+
/**
|
|
12878
|
+
* config.json may carry the permissions/hooks features' keys, so it is never
|
|
12879
|
+
* deleted; only the managed `mcpServers` key is rewritten.
|
|
12880
|
+
*/
|
|
12881
|
+
isDeletable() {
|
|
12882
|
+
return false;
|
|
12883
|
+
}
|
|
11766
12884
|
static getSettablePaths({ global = false } = {}) {
|
|
11767
12885
|
if (global) return {
|
|
11768
|
-
relativeDirPath:
|
|
11769
|
-
relativeFilePath:
|
|
12886
|
+
relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
|
|
12887
|
+
relativeFilePath: DEVIN_CONFIG_FILE_NAME
|
|
11770
12888
|
};
|
|
11771
12889
|
return {
|
|
11772
|
-
relativeDirPath:
|
|
11773
|
-
relativeFilePath:
|
|
12890
|
+
relativeDirPath: DEVIN_DIR,
|
|
12891
|
+
relativeFilePath: DEVIN_CONFIG_FILE_NAME
|
|
11774
12892
|
};
|
|
11775
12893
|
}
|
|
11776
12894
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
@@ -11885,7 +13003,7 @@ var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
|
|
|
11885
13003
|
};
|
|
11886
13004
|
//#endregion
|
|
11887
13005
|
//#region src/features/mcp/goose-mcp.ts
|
|
11888
|
-
const
|
|
13006
|
+
const GOOSE_PLUGIN_MCP_RELATIVE_PATH = (0, node_path.join)(GOOSE_PLUGIN_MCP_DIR_PATH, GOOSE_PLUGIN_MCP_FILE_NAME);
|
|
11889
13007
|
function parseGooseConfig(fileContent, relativeDirPath, relativeFilePath) {
|
|
11890
13008
|
const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
|
|
11891
13009
|
let parsed;
|
|
@@ -11972,7 +13090,7 @@ function convertServerToGooseExtension(name, config) {
|
|
|
11972
13090
|
function convertToGooseFormat(mcpServers) {
|
|
11973
13091
|
const extensions = {};
|
|
11974
13092
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
11975
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
13093
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
11976
13094
|
extensions[name] = convertServerToGooseExtension(name, config);
|
|
11977
13095
|
}
|
|
11978
13096
|
return extensions;
|
|
@@ -11989,7 +13107,7 @@ function convertToGooseFormat(mcpServers) {
|
|
|
11989
13107
|
function convertFromGooseFormat(extensions) {
|
|
11990
13108
|
const result = {};
|
|
11991
13109
|
for (const [name, ext] of Object.entries(extensions)) {
|
|
11992
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
13110
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(ext)) continue;
|
|
11993
13111
|
const server = {};
|
|
11994
13112
|
const type = typeof ext.type === "string" ? ext.type : void 0;
|
|
11995
13113
|
if (type === "sse") server.type = "sse";
|
|
@@ -12007,39 +13125,102 @@ function convertFromGooseFormat(extensions) {
|
|
|
12007
13125
|
return result;
|
|
12008
13126
|
}
|
|
12009
13127
|
/**
|
|
13128
|
+
* Builds a Claude-style stdio server entry (`command`/`args`/`env`/`cwd`) for the
|
|
13129
|
+
* open-plugins `.mcp.json` manifest.
|
|
13130
|
+
*/
|
|
13131
|
+
function buildGoosePluginStdioServer(config) {
|
|
13132
|
+
const server = {};
|
|
13133
|
+
const command = config.command;
|
|
13134
|
+
if (Array.isArray(command)) {
|
|
13135
|
+
if (typeof command[0] === "string") server.command = command[0];
|
|
13136
|
+
const rest = command.slice(1).filter((c) => typeof c === "string");
|
|
13137
|
+
const args = isStringArray(config.args) ? config.args : [];
|
|
13138
|
+
if (rest.length > 0 || args.length > 0) server.args = [...rest, ...args];
|
|
13139
|
+
} else if (typeof command === "string") {
|
|
13140
|
+
server.command = command;
|
|
13141
|
+
if (isStringArray(config.args)) server.args = config.args;
|
|
13142
|
+
}
|
|
13143
|
+
if (isPlainObject(config.env)) server.env = omitPrototypePollutionKeys(config.env);
|
|
13144
|
+
if (typeof config.cwd === "string") server.cwd = config.cwd;
|
|
13145
|
+
return server;
|
|
13146
|
+
}
|
|
13147
|
+
/**
|
|
13148
|
+
* Converts rulesync canonical MCP servers into the Claude-style `mcpServers` map
|
|
13149
|
+
* used by Goose open-plugin manifests (`.agents/plugins/rulesync/.mcp.json`).
|
|
13150
|
+
*
|
|
13151
|
+
* The open-plugins manifest is **stdio-only** (no `url`/`headers`), so remote
|
|
13152
|
+
* (http/sse/streamable_http) and `builtin` servers cannot be represented. They
|
|
13153
|
+
* are skipped with a warning rather than silently dropped — remote servers stay
|
|
13154
|
+
* global-config-only (sync them with `--global` to `~/.config/goose/config.yaml`).
|
|
13155
|
+
*/
|
|
13156
|
+
function convertToGoosePluginMcpServers(mcpServers, logger) {
|
|
13157
|
+
const result = {};
|
|
13158
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
13159
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
13160
|
+
const gooseType = resolveGooseType(config, resolveGooseUrl(config));
|
|
13161
|
+
if (gooseType !== "stdio") {
|
|
13162
|
+
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.`);
|
|
13163
|
+
continue;
|
|
13164
|
+
}
|
|
13165
|
+
result[name] = buildGoosePluginStdioServer(config);
|
|
13166
|
+
}
|
|
13167
|
+
return result;
|
|
13168
|
+
}
|
|
13169
|
+
/**
|
|
12010
13170
|
* Goose MCP servers.
|
|
12011
13171
|
*
|
|
12012
|
-
* Goose configures MCP servers
|
|
12013
|
-
*
|
|
12014
|
-
*
|
|
12015
|
-
*
|
|
12016
|
-
*
|
|
13172
|
+
* Goose configures MCP servers in two locations:
|
|
13173
|
+
*
|
|
13174
|
+
* - **Global** (`--global`): "extensions" in the shared user config
|
|
13175
|
+
* `~/.config/goose/config.yaml`. That file also holds other Goose settings
|
|
13176
|
+
* (model, provider, ...), so generation merges the `extensions:` block into the
|
|
13177
|
+
* existing config instead of overwriting it, and the file is never deleted.
|
|
13178
|
+
* This location supports both stdio and remote (http/sse) servers.
|
|
13179
|
+
* - **Project**: a stdio-only open-plugin manifest at
|
|
13180
|
+
* `.agents/plugins/rulesync/.mcp.json` (Goose v1.39.0+), reusing the same
|
|
13181
|
+
* `.agents/plugins/rulesync/` tree as Goose hooks. The manifest uses the
|
|
13182
|
+
* Claude-style `{ "mcpServers": { "<name>": { command, args, env, cwd } } }`
|
|
13183
|
+
* shape and cannot express `url`/`headers`, so remote servers are skipped with
|
|
13184
|
+
* a warning in project mode (use `--global` to sync them instead).
|
|
12017
13185
|
*
|
|
12018
13186
|
* @see https://block.github.io/goose/docs/getting-started/using-extensions/
|
|
13187
|
+
* @see https://github.com/block/goose/pull/9471
|
|
12019
13188
|
*/
|
|
12020
13189
|
var GooseMcp = class GooseMcp extends ToolMcp {
|
|
12021
13190
|
config;
|
|
12022
13191
|
constructor(params) {
|
|
12023
13192
|
super(params);
|
|
12024
|
-
if (this.fileContent
|
|
12025
|
-
else this.config =
|
|
13193
|
+
if (this.fileContent === void 0) this.config = {};
|
|
13194
|
+
else if (params.global) this.config = parseGooseConfig(this.fileContent, this.relativeDirPath, this.relativeFilePath);
|
|
13195
|
+
else this.config = this.fileContent ? this.parsePluginManifest(this.fileContent) : {};
|
|
13196
|
+
}
|
|
13197
|
+
parsePluginManifest(fileContent) {
|
|
13198
|
+
try {
|
|
13199
|
+
const parsed = JSON.parse(fileContent);
|
|
13200
|
+
return isRecord(parsed) ? parsed : {};
|
|
13201
|
+
} catch (error) {
|
|
13202
|
+
throw new Error(`Failed to parse Goose MCP manifest at ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(error)}`, { cause: error });
|
|
13203
|
+
}
|
|
12026
13204
|
}
|
|
12027
13205
|
getConfig() {
|
|
12028
13206
|
return this.config;
|
|
12029
13207
|
}
|
|
12030
13208
|
isDeletable() {
|
|
12031
|
-
return
|
|
13209
|
+
return !this.global;
|
|
12032
13210
|
}
|
|
12033
|
-
static getSettablePaths(
|
|
12034
|
-
return {
|
|
13211
|
+
static getSettablePaths({ global = false } = {}) {
|
|
13212
|
+
if (global) return {
|
|
12035
13213
|
relativeDirPath: GOOSE_GLOBAL_DIR,
|
|
12036
13214
|
relativeFilePath: GOOSE_MCP_FILE_NAME
|
|
12037
13215
|
};
|
|
13216
|
+
return {
|
|
13217
|
+
relativeDirPath: GOOSE_PLUGIN_MCP_DIR_PATH,
|
|
13218
|
+
relativeFilePath: GOOSE_PLUGIN_MCP_FILE_NAME
|
|
13219
|
+
};
|
|
12038
13220
|
}
|
|
12039
13221
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
12040
|
-
if (!global) throw new Error(GOOSE_GLOBAL_ONLY_MESSAGE$1);
|
|
12041
13222
|
const paths = this.getSettablePaths({ global });
|
|
12042
|
-
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
13223
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? (global ? "" : JSON.stringify({ mcpServers: {} }, null, 2));
|
|
12043
13224
|
return new GooseMcp({
|
|
12044
13225
|
outputRoot,
|
|
12045
13226
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -12049,9 +13230,19 @@ var GooseMcp = class GooseMcp extends ToolMcp {
|
|
|
12049
13230
|
global
|
|
12050
13231
|
});
|
|
12051
13232
|
}
|
|
12052
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
12053
|
-
if (!global) throw new Error(GOOSE_GLOBAL_ONLY_MESSAGE$1);
|
|
13233
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
12054
13234
|
const paths = this.getSettablePaths({ global });
|
|
13235
|
+
if (!global) {
|
|
13236
|
+
const mcpServers = convertToGoosePluginMcpServers(rulesyncMcp.getMcpServers(), logger);
|
|
13237
|
+
return new GooseMcp({
|
|
13238
|
+
outputRoot,
|
|
13239
|
+
relativeDirPath: paths.relativeDirPath,
|
|
13240
|
+
relativeFilePath: paths.relativeFilePath,
|
|
13241
|
+
fileContent: JSON.stringify({ mcpServers }, null, 2),
|
|
13242
|
+
validate,
|
|
13243
|
+
global
|
|
13244
|
+
});
|
|
13245
|
+
}
|
|
12055
13246
|
const merged = {
|
|
12056
13247
|
...parseGooseConfig(await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), ""), paths.relativeDirPath, paths.relativeFilePath),
|
|
12057
13248
|
extensions: convertToGooseFormat(rulesyncMcp.getMcpServers())
|
|
@@ -12066,7 +13257,11 @@ var GooseMcp = class GooseMcp extends ToolMcp {
|
|
|
12066
13257
|
});
|
|
12067
13258
|
}
|
|
12068
13259
|
toRulesyncMcp() {
|
|
12069
|
-
|
|
13260
|
+
if (!this.global) {
|
|
13261
|
+
const mcpServers = isRecord(this.config.mcpServers) ? this.config.mcpServers : {};
|
|
13262
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
13263
|
+
}
|
|
13264
|
+
const mcpServers = convertFromGooseFormat(isRecord(this.config.extensions) ? this.config.extensions : {});
|
|
12070
13265
|
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
12071
13266
|
}
|
|
12072
13267
|
validate() {
|
|
@@ -12143,7 +13338,7 @@ function convertToGrokFormat(mcpServers) {
|
|
|
12143
13338
|
const result = {};
|
|
12144
13339
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
12145
13340
|
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
12146
|
-
if (!isRecord
|
|
13341
|
+
if (!isRecord(config)) continue;
|
|
12147
13342
|
const converted = {};
|
|
12148
13343
|
for (const [key, value] of Object.entries(config)) {
|
|
12149
13344
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
@@ -12158,7 +13353,7 @@ function convertToGrokFormat(mcpServers) {
|
|
|
12158
13353
|
function convertFromGrokFormat(grokMcp) {
|
|
12159
13354
|
const result = {};
|
|
12160
13355
|
for (const [name, config] of Object.entries(grokMcp)) {
|
|
12161
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
13356
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
12162
13357
|
const converted = {};
|
|
12163
13358
|
for (const [key, value] of Object.entries(config)) {
|
|
12164
13359
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
@@ -12321,13 +13516,13 @@ function convertServerToHermes(config) {
|
|
|
12321
13516
|
function convertToHermesFormat(mcpServers) {
|
|
12322
13517
|
const result = {};
|
|
12323
13518
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
12324
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
13519
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
12325
13520
|
result[name] = convertServerToHermes(config);
|
|
12326
13521
|
}
|
|
12327
13522
|
return result;
|
|
12328
13523
|
}
|
|
12329
13524
|
function mergeHermesMcpServers(config, mcpServers) {
|
|
12330
|
-
const existingMcpServers = isRecord
|
|
13525
|
+
const existingMcpServers = isRecord(config.mcp_servers) ? config.mcp_servers : {};
|
|
12331
13526
|
return {
|
|
12332
13527
|
...config,
|
|
12333
13528
|
mcp_servers: {
|
|
@@ -12345,7 +13540,7 @@ function mergeHermesMcpServers(config, mcpServers) {
|
|
|
12345
13540
|
function convertFromHermesFormat(mcpServers) {
|
|
12346
13541
|
const result = {};
|
|
12347
13542
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
12348
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
13543
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
12349
13544
|
const server = {};
|
|
12350
13545
|
if (typeof config.command === "string") server.command = config.command;
|
|
12351
13546
|
if (isStringArray(config.args)) server.args = config.args;
|
|
@@ -12381,7 +13576,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
12381
13576
|
return true;
|
|
12382
13577
|
}
|
|
12383
13578
|
setFileContent(fileContent) {
|
|
12384
|
-
const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord
|
|
13579
|
+
const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
12385
13580
|
this.config = merged;
|
|
12386
13581
|
super.setFileContent(stringifyHermesConfig(merged));
|
|
12387
13582
|
}
|
|
@@ -12421,7 +13616,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
12421
13616
|
});
|
|
12422
13617
|
}
|
|
12423
13618
|
toRulesyncMcp() {
|
|
12424
|
-
const servers = convertFromHermesFormat(isRecord
|
|
13619
|
+
const servers = convertFromHermesFormat(isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
12425
13620
|
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: servers }, null, 2) });
|
|
12426
13621
|
}
|
|
12427
13622
|
validate() {
|
|
@@ -13256,11 +14451,6 @@ var QwencodeMcp = class QwencodeMcp extends ToolMcp {
|
|
|
13256
14451
|
}
|
|
13257
14452
|
};
|
|
13258
14453
|
//#endregion
|
|
13259
|
-
//#region src/constants/reasonix-paths.ts
|
|
13260
|
-
const REASONIX_PROJECT_MCP_FILE_NAME = "reasonix.toml";
|
|
13261
|
-
const REASONIX_GLOBAL_DIR = ".reasonix";
|
|
13262
|
-
const REASONIX_GLOBAL_MCP_FILE_NAME = "config.toml";
|
|
13263
|
-
//#endregion
|
|
13264
14454
|
//#region src/features/mcp/reasonix-mcp.ts
|
|
13265
14455
|
const REASONIX_PLUGIN_FIELDS = [
|
|
13266
14456
|
"type",
|
|
@@ -13268,13 +14458,14 @@ const REASONIX_PLUGIN_FIELDS = [
|
|
|
13268
14458
|
"args",
|
|
13269
14459
|
"env",
|
|
13270
14460
|
"url",
|
|
13271
|
-
"headers"
|
|
14461
|
+
"headers",
|
|
14462
|
+
"trusted_read_only_tools"
|
|
13272
14463
|
];
|
|
13273
14464
|
var ReasonixMcp = class ReasonixMcp extends ToolMcp {
|
|
13274
14465
|
toml;
|
|
13275
14466
|
constructor(params) {
|
|
13276
14467
|
super(params);
|
|
13277
|
-
this.toml = parseReasonixConfig(this.fileContent);
|
|
14468
|
+
this.toml = parseReasonixConfig$1(this.fileContent);
|
|
13278
14469
|
}
|
|
13279
14470
|
getToml() {
|
|
13280
14471
|
return this.toml;
|
|
@@ -13298,7 +14489,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
|
|
|
13298
14489
|
}
|
|
13299
14490
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
13300
14491
|
const paths = this.getSettablePaths({ global });
|
|
13301
|
-
const config = parseReasonixConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
|
|
14492
|
+
const config = parseReasonixConfig$1(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
|
|
13302
14493
|
config.plugins = normalizePluginsArray(config.plugins);
|
|
13303
14494
|
return new ReasonixMcp({
|
|
13304
14495
|
outputRoot,
|
|
@@ -13311,7 +14502,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
|
|
|
13311
14502
|
}
|
|
13312
14503
|
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
13313
14504
|
const paths = this.getSettablePaths({ global });
|
|
13314
|
-
const config = parseReasonixConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
|
|
14505
|
+
const config = parseReasonixConfig$1(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
|
|
13315
14506
|
config.plugins = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToReasonix(name, server));
|
|
13316
14507
|
return new ReasonixMcp({
|
|
13317
14508
|
outputRoot,
|
|
@@ -13328,7 +14519,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
|
|
|
13328
14519
|
}
|
|
13329
14520
|
validate() {
|
|
13330
14521
|
try {
|
|
13331
|
-
parseReasonixConfig(this.fileContent);
|
|
14522
|
+
parseReasonixConfig$1(this.fileContent);
|
|
13332
14523
|
return {
|
|
13333
14524
|
success: true,
|
|
13334
14525
|
error: null
|
|
@@ -13351,7 +14542,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
|
|
|
13351
14542
|
});
|
|
13352
14543
|
}
|
|
13353
14544
|
};
|
|
13354
|
-
function parseReasonixConfig(fileContent) {
|
|
14545
|
+
function parseReasonixConfig$1(fileContent) {
|
|
13355
14546
|
const parsed = smol_toml.parse(fileContent || smol_toml.stringify({}));
|
|
13356
14547
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
13357
14548
|
return { ...parsed };
|
|
@@ -13491,17 +14682,6 @@ var RooMcp = class RooMcp extends ToolMcp {
|
|
|
13491
14682
|
}
|
|
13492
14683
|
};
|
|
13493
14684
|
//#endregion
|
|
13494
|
-
//#region src/constants/rovodev-paths.ts
|
|
13495
|
-
const ROVODEV_DIR = ".rovodev";
|
|
13496
|
-
const ROVODEV_SKILLS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "skills");
|
|
13497
|
-
const ROVODEV_SUBAGENTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "subagents");
|
|
13498
|
-
const ROVODEV_MODULAR_RULES_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, ".rulesync", "modular-rules");
|
|
13499
|
-
const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
|
|
13500
|
-
const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
|
|
13501
|
-
const ROVODEV_MCP_FILE_NAME = "mcp.json";
|
|
13502
|
-
const ROVODEV_CONFIG_FILE_NAME = "config.yml";
|
|
13503
|
-
const ROVODEV_AGENTS_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
|
|
13504
|
-
//#endregion
|
|
13505
14685
|
//#region src/features/mcp/rovodev-mcp.ts
|
|
13506
14686
|
function parseRovodevMcpJson(fileContent, relativeDirPath, relativeFilePath) {
|
|
13507
14687
|
const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
|
|
@@ -13595,6 +14775,178 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
13595
14775
|
}
|
|
13596
14776
|
};
|
|
13597
14777
|
//#endregion
|
|
14778
|
+
//#region src/features/shared/takt-config.ts
|
|
14779
|
+
/**
|
|
14780
|
+
* Parse a Takt `config.yaml` into a plain object, treating an empty file as `{}`.
|
|
14781
|
+
*
|
|
14782
|
+
* Shared by the Takt adapters that read-modify-write the same `config.yaml`
|
|
14783
|
+
* (mcp, permissions, ...). Uses `isPlainObject` (not `isRecord`) so class
|
|
14784
|
+
* instances are rejected for prototype-pollution hardening; a YAML mapping
|
|
14785
|
+
* always parses to a plain object.
|
|
14786
|
+
*/
|
|
14787
|
+
function parseTaktConfig(fileContent, relativeDirPath, relativeFilePath) {
|
|
14788
|
+
const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
|
|
14789
|
+
let parsed;
|
|
14790
|
+
try {
|
|
14791
|
+
parsed = fileContent.trim() === "" ? {} : (0, js_yaml.load)(fileContent);
|
|
14792
|
+
} catch (error) {
|
|
14793
|
+
throw new Error(`Failed to parse Takt config at ${configPath}: ${formatError(error)}`, { cause: error });
|
|
14794
|
+
}
|
|
14795
|
+
if (parsed === void 0 || parsed === null) return {};
|
|
14796
|
+
if (!isPlainObject(parsed)) throw new Error(`Failed to parse Takt config at ${configPath}: expected a YAML mapping`);
|
|
14797
|
+
return parsed;
|
|
14798
|
+
}
|
|
14799
|
+
//#endregion
|
|
14800
|
+
//#region src/features/mcp/takt-mcp.ts
|
|
14801
|
+
/**
|
|
14802
|
+
* MCP adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
|
|
14803
|
+
* global).
|
|
14804
|
+
*
|
|
14805
|
+
* IMPORTANT — what this adapter can and cannot represent.
|
|
14806
|
+
*
|
|
14807
|
+
* Takt does NOT have a project- or global-level registry of MCP *server
|
|
14808
|
+
* definitions*. The concrete `mcp_servers` map (a server's `command`/`args`/`env`
|
|
14809
|
+
* or `type`/`url`/`headers`) is declared per-step inside individual *workflow*
|
|
14810
|
+
* YAML files; there is no top-level `mcp_servers` key in `config.yaml`, and the
|
|
14811
|
+
* config loader hard-rejects unknown top-level keys
|
|
14812
|
+
* (`assertNoUnknownGlobalConfigKeys`). Writing a server map into `config.yaml`
|
|
14813
|
+
* would therefore both be ignored and break the user's config.
|
|
14814
|
+
*
|
|
14815
|
+
* What `config.yaml` *does* hold is the default-deny transport allowlist
|
|
14816
|
+
* `workflow_mcp_servers: { stdio, sse, http }`. Without it, workflow-defined MCP
|
|
14817
|
+
* servers are refused regardless of how they are declared. So this adapter emits
|
|
14818
|
+
* the transport allowlist derived from the transports present in
|
|
14819
|
+
* `.rulesync/mcp.json`, enabling exactly the transports the user's servers need.
|
|
14820
|
+
*
|
|
14821
|
+
* Lossiness (documented, intentional): the per-server names, commands, env, URLs
|
|
14822
|
+
* and headers are NOT representable in `config.yaml` and are intentionally not
|
|
14823
|
+
* written. Users still declare the concrete servers in their workflow YAML
|
|
14824
|
+
* steps; rulesync only opens the transport gate that permits them. As a
|
|
14825
|
+
* corollary, import (`toRulesyncMcp`) cannot reconstruct server definitions from
|
|
14826
|
+
* a transport allowlist and yields an empty `mcpServers` map.
|
|
14827
|
+
*
|
|
14828
|
+
* The shared `config.yaml` is merged in place: only the
|
|
14829
|
+
* `workflow_mcp_servers` key is set; every other top-level key (provider,
|
|
14830
|
+
* provider_profiles, etc.) is preserved. The file is never deleted.
|
|
14831
|
+
*
|
|
14832
|
+
* @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
|
|
14833
|
+
* @see https://github.com/nrslib/takt/blob/main/src/core/models/mcp-schemas.ts
|
|
14834
|
+
*/
|
|
14835
|
+
var TaktMcp = class TaktMcp extends ToolMcp {
|
|
14836
|
+
constructor(params) {
|
|
14837
|
+
super({
|
|
14838
|
+
...params,
|
|
14839
|
+
fileContent: params.fileContent ?? ""
|
|
14840
|
+
});
|
|
14841
|
+
}
|
|
14842
|
+
isDeletable() {
|
|
14843
|
+
return false;
|
|
14844
|
+
}
|
|
14845
|
+
static getSettablePaths(_options) {
|
|
14846
|
+
return {
|
|
14847
|
+
relativeDirPath: TAKT_DIR,
|
|
14848
|
+
relativeFilePath: TAKT_CONFIG_FILE_NAME
|
|
14849
|
+
};
|
|
14850
|
+
}
|
|
14851
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
14852
|
+
const paths = TaktMcp.getSettablePaths({ global });
|
|
14853
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
14854
|
+
return new TaktMcp({
|
|
14855
|
+
outputRoot,
|
|
14856
|
+
relativeDirPath: paths.relativeDirPath,
|
|
14857
|
+
relativeFilePath: paths.relativeFilePath,
|
|
14858
|
+
fileContent,
|
|
14859
|
+
validate,
|
|
14860
|
+
global
|
|
14861
|
+
});
|
|
14862
|
+
}
|
|
14863
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
14864
|
+
const paths = TaktMcp.getSettablePaths({ global });
|
|
14865
|
+
const config = parseTaktConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
|
|
14866
|
+
const allowlist = deriveTransportAllowlist(rulesyncMcp.getMcpServers());
|
|
14867
|
+
const merged = {
|
|
14868
|
+
...config,
|
|
14869
|
+
[TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist
|
|
14870
|
+
};
|
|
14871
|
+
return new TaktMcp({
|
|
14872
|
+
outputRoot,
|
|
14873
|
+
relativeDirPath: paths.relativeDirPath,
|
|
14874
|
+
relativeFilePath: paths.relativeFilePath,
|
|
14875
|
+
fileContent: (0, js_yaml.dump)(merged),
|
|
14876
|
+
validate,
|
|
14877
|
+
global
|
|
14878
|
+
});
|
|
14879
|
+
}
|
|
14880
|
+
/**
|
|
14881
|
+
* A transport allowlist cannot reconstruct the per-step server definitions,
|
|
14882
|
+
* so import yields an empty `mcpServers` map. This keeps the round-trip honest
|
|
14883
|
+
* rather than fabricating placeholder servers.
|
|
14884
|
+
*/
|
|
14885
|
+
toRulesyncMcp() {
|
|
14886
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: {} }, null, 2) });
|
|
14887
|
+
}
|
|
14888
|
+
validate() {
|
|
14889
|
+
return {
|
|
14890
|
+
success: true,
|
|
14891
|
+
error: null
|
|
14892
|
+
};
|
|
14893
|
+
}
|
|
14894
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
14895
|
+
return new TaktMcp({
|
|
14896
|
+
outputRoot,
|
|
14897
|
+
relativeDirPath,
|
|
14898
|
+
relativeFilePath,
|
|
14899
|
+
fileContent: "",
|
|
14900
|
+
validate: false,
|
|
14901
|
+
global
|
|
14902
|
+
});
|
|
14903
|
+
}
|
|
14904
|
+
};
|
|
14905
|
+
/**
|
|
14906
|
+
* Map a rulesync MCP server onto the single Takt transport its allowlist gates.
|
|
14907
|
+
*
|
|
14908
|
+
* Takt allows only `stdio` / `sse` / `http`, so the broader rulesync alias set is
|
|
14909
|
+
* folded: `local` ⇒ stdio; `streamable-http` / `ws` ⇒ http. A server with no
|
|
14910
|
+
* explicit transport is treated as stdio when it carries a `command`, else as a
|
|
14911
|
+
* remote `http` server (it must have a `url`). Returns `undefined` only when the
|
|
14912
|
+
* shape is too ambiguous to classify.
|
|
14913
|
+
*/
|
|
14914
|
+
function transportOf(server) {
|
|
14915
|
+
switch (server.type ?? server.transport) {
|
|
14916
|
+
case "stdio":
|
|
14917
|
+
case "local": return "stdio";
|
|
14918
|
+
case "sse": return "sse";
|
|
14919
|
+
case "http":
|
|
14920
|
+
case "streamable-http":
|
|
14921
|
+
case "ws": return "http";
|
|
14922
|
+
default: break;
|
|
14923
|
+
}
|
|
14924
|
+
if (server.command !== void 0) return "stdio";
|
|
14925
|
+
if (server.url !== void 0 || server.httpUrl !== void 0) return "http";
|
|
14926
|
+
}
|
|
14927
|
+
/**
|
|
14928
|
+
* Derive Takt's `workflow_mcp_servers` allowlist from the transports present in
|
|
14929
|
+
* the rulesync servers. All three keys are emitted explicitly (default-deny made
|
|
14930
|
+
* visible): a transport is `true` only when at least one server uses it.
|
|
14931
|
+
*
|
|
14932
|
+
* Server entries are read defensively (record guard); prototype-pollution server
|
|
14933
|
+
* names are irrelevant here because no user-controlled key or value is written —
|
|
14934
|
+
* only the three fixed boolean keys are.
|
|
14935
|
+
*/
|
|
14936
|
+
function deriveTransportAllowlist(servers) {
|
|
14937
|
+
const allowlist = {
|
|
14938
|
+
stdio: false,
|
|
14939
|
+
sse: false,
|
|
14940
|
+
http: false
|
|
14941
|
+
};
|
|
14942
|
+
for (const server of Object.values(servers)) {
|
|
14943
|
+
if (!isRecord(server)) continue;
|
|
14944
|
+
const transport = transportOf(server);
|
|
14945
|
+
if (transport) allowlist[transport] = true;
|
|
14946
|
+
}
|
|
14947
|
+
return allowlist;
|
|
14948
|
+
}
|
|
14949
|
+
//#endregion
|
|
13598
14950
|
//#region src/features/mcp/vibe-mcp.ts
|
|
13599
14951
|
const VIBE_MCP_SERVER_FIELDS = [
|
|
13600
14952
|
"transport",
|
|
@@ -14055,7 +15407,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
14055
15407
|
["goose", {
|
|
14056
15408
|
class: GooseMcp,
|
|
14057
15409
|
meta: {
|
|
14058
|
-
supportsProject:
|
|
15410
|
+
supportsProject: true,
|
|
14059
15411
|
supportsGlobal: true,
|
|
14060
15412
|
supportsEnabledTools: false,
|
|
14061
15413
|
supportsDisabledTools: false
|
|
@@ -14169,6 +15521,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
14169
15521
|
supportsDisabledTools: false
|
|
14170
15522
|
}
|
|
14171
15523
|
}],
|
|
15524
|
+
["takt", {
|
|
15525
|
+
class: TaktMcp,
|
|
15526
|
+
meta: {
|
|
15527
|
+
supportsProject: true,
|
|
15528
|
+
supportsGlobal: true,
|
|
15529
|
+
supportsEnabledTools: false,
|
|
15530
|
+
supportsDisabledTools: false
|
|
15531
|
+
}
|
|
15532
|
+
}],
|
|
14172
15533
|
["vibe", {
|
|
14173
15534
|
class: VibeMcp,
|
|
14174
15535
|
meta: {
|
|
@@ -14297,7 +15658,8 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
14297
15658
|
return await factory.class.fromRulesyncMcp({
|
|
14298
15659
|
outputRoot: this.outputRoot,
|
|
14299
15660
|
rulesyncMcp: filteredRulesyncMcp,
|
|
14300
|
-
global: this.global
|
|
15661
|
+
global: this.global,
|
|
15662
|
+
logger: this.logger
|
|
14301
15663
|
});
|
|
14302
15664
|
}));
|
|
14303
15665
|
}
|
|
@@ -14811,7 +16173,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
|
|
|
14811
16173
|
function toAntigravityCliToolName(canonical) {
|
|
14812
16174
|
return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
|
|
14813
16175
|
}
|
|
14814
|
-
function toCanonicalToolName$
|
|
16176
|
+
function toCanonicalToolName$6(cliName) {
|
|
14815
16177
|
return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
|
|
14816
16178
|
}
|
|
14817
16179
|
/**
|
|
@@ -14994,7 +16356,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
|
|
|
14994
16356
|
const processEntries = (entries, action) => {
|
|
14995
16357
|
for (const entry of entries) {
|
|
14996
16358
|
const { toolName, pattern } = parsePermissionEntry$1(entry);
|
|
14997
|
-
const canonical = toCanonicalToolName$
|
|
16359
|
+
const canonical = toCanonicalToolName$6(toolName);
|
|
14998
16360
|
if (!permission[canonical]) permission[canonical] = {};
|
|
14999
16361
|
permission[canonical][pattern] = action;
|
|
15000
16362
|
}
|
|
@@ -15033,7 +16395,7 @@ const IDE_ACTION_TO_CANONICAL = {
|
|
|
15033
16395
|
function toIdeAction(canonical) {
|
|
15034
16396
|
return CANONICAL_TO_IDE_ACTION[canonical] ?? canonical;
|
|
15035
16397
|
}
|
|
15036
|
-
function toCanonicalCategory$
|
|
16398
|
+
function toCanonicalCategory$2(ideAction) {
|
|
15037
16399
|
return IDE_ACTION_TO_CANONICAL[ideAction] ?? ideAction;
|
|
15038
16400
|
}
|
|
15039
16401
|
/**
|
|
@@ -15207,7 +16569,7 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
|
|
|
15207
16569
|
const processEntries = (entries, action) => {
|
|
15208
16570
|
for (const entry of entries) {
|
|
15209
16571
|
const { action: ideAction, pattern } = parsePermissionEntry(entry);
|
|
15210
|
-
const canonical = toCanonicalCategory$
|
|
16572
|
+
const canonical = toCanonicalCategory$2(ideAction);
|
|
15211
16573
|
if (!permission[canonical]) permission[canonical] = {};
|
|
15212
16574
|
permission[canonical][pattern] = action;
|
|
15213
16575
|
}
|
|
@@ -15256,7 +16618,7 @@ const AUGMENT_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONI
|
|
|
15256
16618
|
function toAugmentToolName(canonical) {
|
|
15257
16619
|
return CANONICAL_TO_AUGMENT_TOOL_NAMES[canonical] ?? canonical;
|
|
15258
16620
|
}
|
|
15259
|
-
function toCanonicalToolName$
|
|
16621
|
+
function toCanonicalToolName$5(augmentName) {
|
|
15260
16622
|
return AUGMENT_TO_CANONICAL_TOOL_NAMES[augmentName] ?? augmentName;
|
|
15261
16623
|
}
|
|
15262
16624
|
function actionToAugmentType(action) {
|
|
@@ -15600,7 +16962,7 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
15600
16962
|
}
|
|
15601
16963
|
const type = entry.permission.type;
|
|
15602
16964
|
if (!isBasicAugmentType(type)) continue;
|
|
15603
|
-
const canonical = toCanonicalToolName$
|
|
16965
|
+
const canonical = toCanonicalToolName$5(entry.toolName);
|
|
15604
16966
|
if (forbiddenMapKeys.has(canonical)) {
|
|
15605
16967
|
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.`);
|
|
15606
16968
|
continue;
|
|
@@ -15653,7 +17015,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
|
|
|
15653
17015
|
function toClaudeToolName(canonical) {
|
|
15654
17016
|
return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
|
|
15655
17017
|
}
|
|
15656
|
-
function toCanonicalToolName$
|
|
17018
|
+
function toCanonicalToolName$4(claudeName) {
|
|
15657
17019
|
return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
|
|
15658
17020
|
}
|
|
15659
17021
|
/**
|
|
@@ -15827,7 +17189,7 @@ function convertClaudeToRulesyncPermissions(params) {
|
|
|
15827
17189
|
const processEntries = (entries, action) => {
|
|
15828
17190
|
for (const entry of entries) {
|
|
15829
17191
|
const { toolName, pattern } = parseClaudePermissionEntry(entry);
|
|
15830
|
-
const canonical = toCanonicalToolName$
|
|
17192
|
+
const canonical = toCanonicalToolName$4(toolName);
|
|
15831
17193
|
if (!permission[canonical]) permission[canonical] = {};
|
|
15832
17194
|
permission[canonical][pattern] = action;
|
|
15833
17195
|
}
|
|
@@ -16454,7 +17816,7 @@ function toCursorPattern(canonical, pattern) {
|
|
|
16454
17816
|
}
|
|
16455
17817
|
return pattern;
|
|
16456
17818
|
}
|
|
16457
|
-
function toCanonicalCategory(cursorType, pattern) {
|
|
17819
|
+
function toCanonicalCategory$1(cursorType, pattern) {
|
|
16458
17820
|
if (cursorType === "Mcp") {
|
|
16459
17821
|
const match = pattern.match(/^([^:()]+):([^:()]+)$/);
|
|
16460
17822
|
if (match) return `${MCP_CANONICAL_PREFIX}${match[1] ?? "*"}__${match[2] ?? "*"}`;
|
|
@@ -16679,7 +18041,7 @@ function convertCursorToRulesyncPermissions(params) {
|
|
|
16679
18041
|
const processEntries = (entries, action) => {
|
|
16680
18042
|
for (const entry of entries) {
|
|
16681
18043
|
const { type, pattern } = parseCursorPermissionEntry(entry);
|
|
16682
|
-
const canonical = toCanonicalCategory(type, pattern);
|
|
18044
|
+
const canonical = toCanonicalCategory$1(type, pattern);
|
|
16683
18045
|
if (!permission[canonical]) permission[canonical] = {};
|
|
16684
18046
|
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX) ? "*" : pattern;
|
|
16685
18047
|
permission[canonical][canonicalPattern] = action;
|
|
@@ -16690,6 +18052,245 @@ function convertCursorToRulesyncPermissions(params) {
|
|
|
16690
18052
|
return { permission };
|
|
16691
18053
|
}
|
|
16692
18054
|
//#endregion
|
|
18055
|
+
//#region src/features/permissions/devin-permissions.ts
|
|
18056
|
+
/**
|
|
18057
|
+
* Mapping from rulesync canonical tool category names to Devin Local permission
|
|
18058
|
+
* scope matchers.
|
|
18059
|
+
*
|
|
18060
|
+
* Devin expresses permissions with scope-based matchers — `Read(glob)`,
|
|
18061
|
+
* `Write(glob)`, `Exec(prefix)`, and `Fetch(pattern)` — plus MCP tool patterns
|
|
18062
|
+
* (`mcp__server__tool`). The canonical `edit` and `write` categories both map
|
|
18063
|
+
* onto Devin's single `Write` scope; on import `Write` maps back to `write`, so
|
|
18064
|
+
* `edit` rules round-trip as `write` (a lossy but documented collapse). Unknown
|
|
18065
|
+
* names (e.g. `mcp__github__list_issues`) pass through verbatim.
|
|
18066
|
+
*
|
|
18067
|
+
* @see https://docs.devin.ai/cli/reference/permissions
|
|
18068
|
+
*/
|
|
18069
|
+
const CANONICAL_TO_DEVIN_SCOPE = {
|
|
18070
|
+
read: "Read",
|
|
18071
|
+
write: "Write",
|
|
18072
|
+
edit: "Write",
|
|
18073
|
+
bash: "Exec",
|
|
18074
|
+
webfetch: "Fetch"
|
|
18075
|
+
};
|
|
18076
|
+
/**
|
|
18077
|
+
* Reverse mapping from Devin scope matchers to rulesync canonical names.
|
|
18078
|
+
*/
|
|
18079
|
+
const DEVIN_SCOPE_TO_CANONICAL = {
|
|
18080
|
+
Read: "read",
|
|
18081
|
+
Write: "write",
|
|
18082
|
+
Exec: "bash",
|
|
18083
|
+
Fetch: "webfetch"
|
|
18084
|
+
};
|
|
18085
|
+
function toDevinScope(canonical) {
|
|
18086
|
+
return CANONICAL_TO_DEVIN_SCOPE[canonical] ?? canonical;
|
|
18087
|
+
}
|
|
18088
|
+
function toCanonicalCategory(devinScope) {
|
|
18089
|
+
return DEVIN_SCOPE_TO_CANONICAL[devinScope] ?? devinScope;
|
|
18090
|
+
}
|
|
18091
|
+
/**
|
|
18092
|
+
* Parse a Devin permission entry like `Read(src/**)` into scope and pattern.
|
|
18093
|
+
* Bare entries (e.g. `Read`, or a whole-tool name like `exec`) yield `*`.
|
|
18094
|
+
*/
|
|
18095
|
+
function parseDevinPermissionEntry(entry) {
|
|
18096
|
+
const parenIndex = entry.indexOf("(");
|
|
18097
|
+
if (parenIndex === -1) return {
|
|
18098
|
+
scope: entry,
|
|
18099
|
+
pattern: "*"
|
|
18100
|
+
};
|
|
18101
|
+
const scope = entry.slice(0, parenIndex);
|
|
18102
|
+
if (!entry.endsWith(")")) return {
|
|
18103
|
+
scope,
|
|
18104
|
+
pattern: "*"
|
|
18105
|
+
};
|
|
18106
|
+
return {
|
|
18107
|
+
scope,
|
|
18108
|
+
pattern: entry.slice(parenIndex + 1, -1) || "*"
|
|
18109
|
+
};
|
|
18110
|
+
}
|
|
18111
|
+
/**
|
|
18112
|
+
* Build a Devin permission entry like `Read(src/**)`. A `*` pattern collapses to
|
|
18113
|
+
* the bare scope (`Read`), matching the whole scope.
|
|
18114
|
+
*/
|
|
18115
|
+
function buildDevinPermissionEntry(scope, pattern) {
|
|
18116
|
+
if (pattern === "*") return scope;
|
|
18117
|
+
return `${scope}(${pattern})`;
|
|
18118
|
+
}
|
|
18119
|
+
/**
|
|
18120
|
+
* Permissions generator for Devin Local (native `.devin/` configuration).
|
|
18121
|
+
*
|
|
18122
|
+
* Maps rulesync permission actions onto Devin's `permissions` block inside its
|
|
18123
|
+
* native config file — `allow` / `deny` / `ask` arrays of scope matchers
|
|
18124
|
+
* (`Read(glob)`, `Write(glob)`, `Exec(prefix)`, `Fetch(pattern)`, plus
|
|
18125
|
+
* `mcp__server__tool` patterns). Devin evaluates the arrays with strict
|
|
18126
|
+
* precedence: `deny` is checked before `ask`, which is checked before `allow`,
|
|
18127
|
+
* so a deny rule always wins.
|
|
18128
|
+
*
|
|
18129
|
+
* - Project scope: `.devin/config.json`
|
|
18130
|
+
* - Global scope: `~/.config/devin/config.json`
|
|
18131
|
+
*
|
|
18132
|
+
* The config file is shared with the MCP (`mcpServers`) and, in global mode, the
|
|
18133
|
+
* hooks (`hooks`) features, so reads and writes merge into the existing JSON and
|
|
18134
|
+
* the file is never deleted; only the managed `permissions` key is rewritten.
|
|
18135
|
+
*
|
|
18136
|
+
* @see https://docs.devin.ai/cli/reference/permissions
|
|
18137
|
+
*/
|
|
18138
|
+
var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
18139
|
+
constructor(params) {
|
|
18140
|
+
super({
|
|
18141
|
+
...params,
|
|
18142
|
+
fileContent: params.fileContent ?? "{}"
|
|
18143
|
+
});
|
|
18144
|
+
}
|
|
18145
|
+
/**
|
|
18146
|
+
* config.json may carry the MCP/hooks features' keys, so it is never deleted;
|
|
18147
|
+
* only the managed `permissions` key is rewritten.
|
|
18148
|
+
*/
|
|
18149
|
+
isDeletable() {
|
|
18150
|
+
return false;
|
|
18151
|
+
}
|
|
18152
|
+
static getSettablePaths({ global = false } = {}) {
|
|
18153
|
+
if (global) return {
|
|
18154
|
+
relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
|
|
18155
|
+
relativeFilePath: DEVIN_CONFIG_FILE_NAME
|
|
18156
|
+
};
|
|
18157
|
+
return {
|
|
18158
|
+
relativeDirPath: DEVIN_DIR,
|
|
18159
|
+
relativeFilePath: DEVIN_CONFIG_FILE_NAME
|
|
18160
|
+
};
|
|
18161
|
+
}
|
|
18162
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
18163
|
+
const paths = DevinPermissions.getSettablePaths({ global });
|
|
18164
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"permissions\":{}}";
|
|
18165
|
+
return new DevinPermissions({
|
|
18166
|
+
outputRoot,
|
|
18167
|
+
relativeDirPath: paths.relativeDirPath,
|
|
18168
|
+
relativeFilePath: paths.relativeFilePath,
|
|
18169
|
+
fileContent,
|
|
18170
|
+
validate
|
|
18171
|
+
});
|
|
18172
|
+
}
|
|
18173
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, validate = true }) {
|
|
18174
|
+
const paths = DevinPermissions.getSettablePaths({ global });
|
|
18175
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
18176
|
+
const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
|
|
18177
|
+
let settings;
|
|
18178
|
+
try {
|
|
18179
|
+
const parsed = JSON.parse(existingContent);
|
|
18180
|
+
settings = isRecord(parsed) ? parsed : {};
|
|
18181
|
+
} catch (error) {
|
|
18182
|
+
throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
18183
|
+
}
|
|
18184
|
+
const config = rulesyncPermissions.getJson();
|
|
18185
|
+
const { allow, ask, deny } = convertRulesyncToDevinPermissions(config);
|
|
18186
|
+
const managedScopes = new Set(Object.keys(config.permission).map((category) => toDevinScope(category)));
|
|
18187
|
+
const existingPermissions = isRecord(settings.permissions) ? settings.permissions : {};
|
|
18188
|
+
const preserve = (entries) => (entries ?? []).filter((entry) => !managedScopes.has(parseDevinPermissionEntry(entry).scope));
|
|
18189
|
+
const mergedAllow = (0, es_toolkit.uniq)([...preserve(existingPermissions.allow), ...allow].toSorted());
|
|
18190
|
+
const mergedAsk = (0, es_toolkit.uniq)([...preserve(existingPermissions.ask), ...ask].toSorted());
|
|
18191
|
+
const mergedDeny = (0, es_toolkit.uniq)([...preserve(existingPermissions.deny), ...deny].toSorted());
|
|
18192
|
+
const mergedPermissions = { ...existingPermissions };
|
|
18193
|
+
if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
|
|
18194
|
+
else delete mergedPermissions.allow;
|
|
18195
|
+
if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
|
|
18196
|
+
else delete mergedPermissions.ask;
|
|
18197
|
+
if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
|
|
18198
|
+
else delete mergedPermissions.deny;
|
|
18199
|
+
const merged = {
|
|
18200
|
+
...settings,
|
|
18201
|
+
permissions: mergedPermissions
|
|
18202
|
+
};
|
|
18203
|
+
return new DevinPermissions({
|
|
18204
|
+
outputRoot,
|
|
18205
|
+
relativeDirPath: paths.relativeDirPath,
|
|
18206
|
+
relativeFilePath: paths.relativeFilePath,
|
|
18207
|
+
fileContent: JSON.stringify(merged, null, 2),
|
|
18208
|
+
validate
|
|
18209
|
+
});
|
|
18210
|
+
}
|
|
18211
|
+
toRulesyncPermissions() {
|
|
18212
|
+
let settings;
|
|
18213
|
+
try {
|
|
18214
|
+
const parsed = JSON.parse(this.getFileContent());
|
|
18215
|
+
settings = isRecord(parsed) ? parsed : {};
|
|
18216
|
+
} catch (error) {
|
|
18217
|
+
throw new Error(`Failed to parse Devin permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18218
|
+
}
|
|
18219
|
+
const permissions = isRecord(settings.permissions) ? settings.permissions : {};
|
|
18220
|
+
const config = convertDevinToRulesyncPermissions({
|
|
18221
|
+
allow: Array.isArray(permissions.allow) ? permissions.allow : [],
|
|
18222
|
+
ask: Array.isArray(permissions.ask) ? permissions.ask : [],
|
|
18223
|
+
deny: Array.isArray(permissions.deny) ? permissions.deny : []
|
|
18224
|
+
});
|
|
18225
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
|
|
18226
|
+
}
|
|
18227
|
+
validate() {
|
|
18228
|
+
return {
|
|
18229
|
+
success: true,
|
|
18230
|
+
error: null
|
|
18231
|
+
};
|
|
18232
|
+
}
|
|
18233
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
18234
|
+
return new DevinPermissions({
|
|
18235
|
+
outputRoot,
|
|
18236
|
+
relativeDirPath,
|
|
18237
|
+
relativeFilePath,
|
|
18238
|
+
fileContent: JSON.stringify({ permissions: {} }, null, 2),
|
|
18239
|
+
validate: false
|
|
18240
|
+
});
|
|
18241
|
+
}
|
|
18242
|
+
};
|
|
18243
|
+
/**
|
|
18244
|
+
* Convert rulesync permissions config to Devin allow/ask/deny arrays.
|
|
18245
|
+
*/
|
|
18246
|
+
function convertRulesyncToDevinPermissions(config) {
|
|
18247
|
+
const allow = [];
|
|
18248
|
+
const ask = [];
|
|
18249
|
+
const deny = [];
|
|
18250
|
+
for (const [category, rules] of Object.entries(config.permission)) {
|
|
18251
|
+
const scope = toDevinScope(category);
|
|
18252
|
+
for (const [pattern, action] of Object.entries(rules)) {
|
|
18253
|
+
const entry = buildDevinPermissionEntry(scope, pattern);
|
|
18254
|
+
switch (action) {
|
|
18255
|
+
case "allow":
|
|
18256
|
+
allow.push(entry);
|
|
18257
|
+
break;
|
|
18258
|
+
case "ask":
|
|
18259
|
+
ask.push(entry);
|
|
18260
|
+
break;
|
|
18261
|
+
case "deny":
|
|
18262
|
+
deny.push(entry);
|
|
18263
|
+
break;
|
|
18264
|
+
}
|
|
18265
|
+
}
|
|
18266
|
+
}
|
|
18267
|
+
return {
|
|
18268
|
+
allow,
|
|
18269
|
+
ask,
|
|
18270
|
+
deny
|
|
18271
|
+
};
|
|
18272
|
+
}
|
|
18273
|
+
/**
|
|
18274
|
+
* Convert Devin allow/ask/deny arrays to rulesync permissions config. Entries
|
|
18275
|
+
* are applied allow → ask → deny so the most restrictive action wins for a
|
|
18276
|
+
* given (scope, pattern), mirroring Devin's deny > ask > allow precedence.
|
|
18277
|
+
*/
|
|
18278
|
+
function convertDevinToRulesyncPermissions(params) {
|
|
18279
|
+
const permission = {};
|
|
18280
|
+
const processEntries = (entries, action) => {
|
|
18281
|
+
for (const entry of entries) {
|
|
18282
|
+
const { scope, pattern } = parseDevinPermissionEntry(entry);
|
|
18283
|
+
if (isPrototypePollutionKey(scope) || isPrototypePollutionKey(pattern)) continue;
|
|
18284
|
+
const canonical = toCanonicalCategory(scope);
|
|
18285
|
+
(permission[canonical] ??= {})[pattern] = action;
|
|
18286
|
+
}
|
|
18287
|
+
};
|
|
18288
|
+
processEntries(params.allow, "allow");
|
|
18289
|
+
processEntries(params.ask, "ask");
|
|
18290
|
+
processEntries(params.deny, "deny");
|
|
18291
|
+
return { permission };
|
|
18292
|
+
}
|
|
18293
|
+
//#endregion
|
|
16693
18294
|
//#region src/features/permissions/factorydroid-permissions.ts
|
|
16694
18295
|
/**
|
|
16695
18296
|
* Permissions adapter for Factory Droid.
|
|
@@ -16949,7 +18550,7 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
|
|
|
16949
18550
|
} catch (error) {
|
|
16950
18551
|
throw new Error(`Failed to parse existing Goose permission.yaml at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
16951
18552
|
}
|
|
16952
|
-
const config = isRecord
|
|
18553
|
+
const config = isRecord(parsed) ? { ...parsed } : {};
|
|
16953
18554
|
config[GOOSE_USER_KEY] = convertRulesyncToGoosePermissionConfig({
|
|
16954
18555
|
config: rulesyncPermissions.getJson(),
|
|
16955
18556
|
logger
|
|
@@ -16971,8 +18572,8 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
|
|
|
16971
18572
|
} catch (error) {
|
|
16972
18573
|
throw new Error(`Failed to parse Goose permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
16973
18574
|
}
|
|
16974
|
-
const config = isRecord
|
|
16975
|
-
const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord
|
|
18575
|
+
const config = isRecord(parsed) ? parsed : {};
|
|
18576
|
+
const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
|
|
16976
18577
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
16977
18578
|
}
|
|
16978
18579
|
validate() {
|
|
@@ -17112,7 +18713,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
17112
18713
|
throw new Error(`Failed to parse existing Grok config.toml at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
17113
18714
|
}
|
|
17114
18715
|
const mode = deriveGrokPermissionMode(rulesyncPermissions.getJson());
|
|
17115
|
-
const existingUi = isRecord
|
|
18716
|
+
const existingUi = isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
|
|
17116
18717
|
parsed[GROKCLI_UI_KEY] = {
|
|
17117
18718
|
...existingUi,
|
|
17118
18719
|
[GROKCLI_PERMISSION_MODE_KEY]: mode
|
|
@@ -17134,7 +18735,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
17134
18735
|
} catch (error) {
|
|
17135
18736
|
throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
17136
18737
|
}
|
|
17137
|
-
const action = (isRecord
|
|
18738
|
+
const action = (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
|
|
17138
18739
|
const rulesyncConfig = { permission: { bash: { [CATCH_ALL_PATTERN$2]: action } } };
|
|
17139
18740
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
17140
18741
|
}
|
|
@@ -17216,6 +18817,205 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
17216
18817
|
}
|
|
17217
18818
|
};
|
|
17218
18819
|
//#endregion
|
|
18820
|
+
//#region src/features/permissions/junie-permissions.ts
|
|
18821
|
+
/**
|
|
18822
|
+
* JetBrains Junie CLI Action Allowlist (`allowlist.json`).
|
|
18823
|
+
*
|
|
18824
|
+
* Junie gates actions through an allowlist evaluated top-to-bottom (first match
|
|
18825
|
+
* wins). Project scope lives in `.junie/allowlist.json`; user scope lives in
|
|
18826
|
+
* `~/.junie/allowlist.json`.
|
|
18827
|
+
*
|
|
18828
|
+
* ```json
|
|
18829
|
+
* {
|
|
18830
|
+
* "defaultBehavior": "ask",
|
|
18831
|
+
* "allowReadonlyCommands": true,
|
|
18832
|
+
* "rules": {
|
|
18833
|
+
* "executables": [ { "prefix": "git ", "action": "allow" } ],
|
|
18834
|
+
* "fileEditing": [ { "pattern": "src/**", "action": "allow" } ],
|
|
18835
|
+
* "mcpTools": [ { "prefix": "search", "action": "allow" } ],
|
|
18836
|
+
* "readOutsideProject":[ { "pattern": "/etc/**", "action": "deny" } ]
|
|
18837
|
+
* }
|
|
18838
|
+
* }
|
|
18839
|
+
* ```
|
|
18840
|
+
*
|
|
18841
|
+
* Each rule carries a literal `prefix` (matches commands that start with it) or
|
|
18842
|
+
* a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action`
|
|
18843
|
+
* (`allow` | `ask` | `deny`). rulesync's canonical actions map 1:1 onto Junie's.
|
|
18844
|
+
*
|
|
18845
|
+
* Category mapping (rulesync canonical <-> Junie rule group):
|
|
18846
|
+
* - `bash` <-> `executables`
|
|
18847
|
+
* - `edit`/`write` -> `fileEditing` (imported back as `edit`)
|
|
18848
|
+
* - `read` <-> `readOutsideProject`
|
|
18849
|
+
* - `mcp` <-> `mcpTools`
|
|
18850
|
+
*
|
|
18851
|
+
* Categories Junie cannot represent (e.g. `webfetch`) are skipped on export
|
|
18852
|
+
* (with a warning when they carry rules). The top-level `defaultBehavior` and
|
|
18853
|
+
* `allowReadonlyCommands` settings have no canonical equivalent: they are
|
|
18854
|
+
* preserved verbatim on export but not imported into the rulesync model.
|
|
18855
|
+
*
|
|
18856
|
+
* @see https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html
|
|
18857
|
+
*/
|
|
18858
|
+
const JUNIE_RULE_GROUPS = [
|
|
18859
|
+
"executables",
|
|
18860
|
+
"fileEditing",
|
|
18861
|
+
"mcpTools",
|
|
18862
|
+
"readOutsideProject"
|
|
18863
|
+
];
|
|
18864
|
+
const CANONICAL_TO_JUNIE_GROUP = {
|
|
18865
|
+
bash: "executables",
|
|
18866
|
+
edit: "fileEditing",
|
|
18867
|
+
write: "fileEditing",
|
|
18868
|
+
read: "readOutsideProject",
|
|
18869
|
+
mcp: "mcpTools"
|
|
18870
|
+
};
|
|
18871
|
+
const JUNIE_GROUP_TO_CANONICAL = {
|
|
18872
|
+
executables: "bash",
|
|
18873
|
+
fileEditing: "edit",
|
|
18874
|
+
mcpTools: "mcp",
|
|
18875
|
+
readOutsideProject: "read"
|
|
18876
|
+
};
|
|
18877
|
+
const JUNIE_DEFAULT_BEHAVIOR = "ask";
|
|
18878
|
+
function isPermissionAction$1(value) {
|
|
18879
|
+
return PermissionActionSchema.safeParse(value).success;
|
|
18880
|
+
}
|
|
18881
|
+
/**
|
|
18882
|
+
* Whether a rulesync pattern uses glob syntax. Junie expresses literal
|
|
18883
|
+
* "starts-with" matches as `prefix` and glob matches as `pattern`, so a pattern
|
|
18884
|
+
* containing any glob metacharacter (`*`, `?`, `[`) is emitted as `pattern`.
|
|
18885
|
+
*/
|
|
18886
|
+
function isGlobPattern(pattern) {
|
|
18887
|
+
return /[*?[]/.test(pattern);
|
|
18888
|
+
}
|
|
18889
|
+
var JuniePermissions = class JuniePermissions extends ToolPermissions {
|
|
18890
|
+
constructor(params) {
|
|
18891
|
+
super({
|
|
18892
|
+
...params,
|
|
18893
|
+
fileContent: params.fileContent ?? "{}"
|
|
18894
|
+
});
|
|
18895
|
+
}
|
|
18896
|
+
isDeletable() {
|
|
18897
|
+
return false;
|
|
18898
|
+
}
|
|
18899
|
+
static getSettablePaths(_options = {}) {
|
|
18900
|
+
return {
|
|
18901
|
+
relativeDirPath: JUNIE_DIR,
|
|
18902
|
+
relativeFilePath: JUNIE_PERMISSIONS_FILE_NAME
|
|
18903
|
+
};
|
|
18904
|
+
}
|
|
18905
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
18906
|
+
const paths = JuniePermissions.getSettablePaths({ global });
|
|
18907
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
|
|
18908
|
+
return new JuniePermissions({
|
|
18909
|
+
outputRoot,
|
|
18910
|
+
relativeDirPath: paths.relativeDirPath,
|
|
18911
|
+
relativeFilePath: paths.relativeFilePath,
|
|
18912
|
+
fileContent,
|
|
18913
|
+
validate
|
|
18914
|
+
});
|
|
18915
|
+
}
|
|
18916
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
18917
|
+
const paths = JuniePermissions.getSettablePaths({ global });
|
|
18918
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
18919
|
+
const existingContent = await readOrInitializeFileContent(filePath, "{}");
|
|
18920
|
+
let existing;
|
|
18921
|
+
try {
|
|
18922
|
+
const parsed = JSON.parse(existingContent);
|
|
18923
|
+
existing = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
18924
|
+
} catch (error) {
|
|
18925
|
+
throw new Error(`Failed to parse existing Junie allowlist at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
18926
|
+
}
|
|
18927
|
+
const rules = convertRulesyncToJunieRules({
|
|
18928
|
+
config: rulesyncPermissions.getJson(),
|
|
18929
|
+
logger
|
|
18930
|
+
});
|
|
18931
|
+
const merged = {
|
|
18932
|
+
...existing,
|
|
18933
|
+
defaultBehavior: existing.defaultBehavior ?? JUNIE_DEFAULT_BEHAVIOR,
|
|
18934
|
+
rules
|
|
18935
|
+
};
|
|
18936
|
+
return new JuniePermissions({
|
|
18937
|
+
outputRoot,
|
|
18938
|
+
relativeDirPath: paths.relativeDirPath,
|
|
18939
|
+
relativeFilePath: paths.relativeFilePath,
|
|
18940
|
+
fileContent: JSON.stringify(merged, null, 2),
|
|
18941
|
+
validate: true
|
|
18942
|
+
});
|
|
18943
|
+
}
|
|
18944
|
+
toRulesyncPermissions() {
|
|
18945
|
+
let allowlist;
|
|
18946
|
+
try {
|
|
18947
|
+
const parsed = JSON.parse(this.getFileContent());
|
|
18948
|
+
allowlist = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
18949
|
+
} catch (error) {
|
|
18950
|
+
throw new Error(`Failed to parse Junie permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18951
|
+
}
|
|
18952
|
+
const config = convertJunieToRulesyncPermissions({ allowlist });
|
|
18953
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
|
|
18954
|
+
}
|
|
18955
|
+
validate() {
|
|
18956
|
+
return {
|
|
18957
|
+
success: true,
|
|
18958
|
+
error: null
|
|
18959
|
+
};
|
|
18960
|
+
}
|
|
18961
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
18962
|
+
return new JuniePermissions({
|
|
18963
|
+
outputRoot,
|
|
18964
|
+
relativeDirPath,
|
|
18965
|
+
relativeFilePath,
|
|
18966
|
+
fileContent: "{}",
|
|
18967
|
+
validate: false
|
|
18968
|
+
});
|
|
18969
|
+
}
|
|
18970
|
+
};
|
|
18971
|
+
/**
|
|
18972
|
+
* Convert rulesync permissions config into Junie's `rules` object. Categories
|
|
18973
|
+
* with no Junie rule group (e.g. `webfetch`) are skipped, with a warning when
|
|
18974
|
+
* they carry any rule so the gap is surfaced.
|
|
18975
|
+
*/
|
|
18976
|
+
function convertRulesyncToJunieRules({ config, logger }) {
|
|
18977
|
+
const rules = {};
|
|
18978
|
+
for (const [category, patterns] of Object.entries(config.permission)) {
|
|
18979
|
+
const group = CANONICAL_TO_JUNIE_GROUP[category];
|
|
18980
|
+
if (!group) {
|
|
18981
|
+
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.`);
|
|
18982
|
+
continue;
|
|
18983
|
+
}
|
|
18984
|
+
for (const [pattern, action] of Object.entries(patterns)) {
|
|
18985
|
+
const rule = isGlobPattern(pattern) ? {
|
|
18986
|
+
pattern,
|
|
18987
|
+
action
|
|
18988
|
+
} : {
|
|
18989
|
+
prefix: pattern,
|
|
18990
|
+
action
|
|
18991
|
+
};
|
|
18992
|
+
(rules[group] ??= []).push(rule);
|
|
18993
|
+
}
|
|
18994
|
+
}
|
|
18995
|
+
return rules;
|
|
18996
|
+
}
|
|
18997
|
+
/**
|
|
18998
|
+
* Convert a Junie allowlist back into rulesync permissions config. The
|
|
18999
|
+
* top-level `defaultBehavior` / `allowReadonlyCommands` settings have no
|
|
19000
|
+
* canonical equivalent and are not imported.
|
|
19001
|
+
*/
|
|
19002
|
+
function convertJunieToRulesyncPermissions({ allowlist }) {
|
|
19003
|
+
const permission = {};
|
|
19004
|
+
const rules = allowlist.rules;
|
|
19005
|
+
if (rules && typeof rules === "object") for (const group of JUNIE_RULE_GROUPS) {
|
|
19006
|
+
const list = rules[group];
|
|
19007
|
+
if (!Array.isArray(list)) continue;
|
|
19008
|
+
const category = JUNIE_GROUP_TO_CANONICAL[group];
|
|
19009
|
+
for (const rule of list) {
|
|
19010
|
+
if (!rule || typeof rule !== "object") continue;
|
|
19011
|
+
const pattern = typeof rule.pattern === "string" ? rule.pattern : typeof rule.prefix === "string" ? rule.prefix : void 0;
|
|
19012
|
+
if (pattern === void 0 || !isPermissionAction$1(rule.action)) continue;
|
|
19013
|
+
(permission[category] ??= {})[pattern] = rule.action;
|
|
19014
|
+
}
|
|
19015
|
+
}
|
|
19016
|
+
return { permission };
|
|
19017
|
+
}
|
|
19018
|
+
//#endregion
|
|
17219
19019
|
//#region src/features/permissions/kilo-permissions.ts
|
|
17220
19020
|
const KiloPermissionSchema = zod_mini.z.union([zod_mini.z.enum([
|
|
17221
19021
|
"allow",
|
|
@@ -17564,7 +19364,11 @@ const OpencodePermissionSchema = zod_mini.z.union([zod_mini.z.enum([
|
|
|
17564
19364
|
"ask",
|
|
17565
19365
|
"deny"
|
|
17566
19366
|
]))]);
|
|
17567
|
-
const OpencodePermissionsConfigSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.
|
|
19367
|
+
const OpencodePermissionsConfigSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.enum([
|
|
19368
|
+
"allow",
|
|
19369
|
+
"ask",
|
|
19370
|
+
"deny"
|
|
19371
|
+
]), zod_mini.z.record(zod_mini.z.string(), OpencodePermissionSchema)])) });
|
|
17568
19372
|
var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
17569
19373
|
json;
|
|
17570
19374
|
constructor(params) {
|
|
@@ -17667,6 +19471,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
17667
19471
|
}
|
|
17668
19472
|
normalizePermission(permission) {
|
|
17669
19473
|
if (!permission) return {};
|
|
19474
|
+
if (typeof permission === "string") return { "*": { "*": permission } };
|
|
17670
19475
|
return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
|
|
17671
19476
|
}
|
|
17672
19477
|
};
|
|
@@ -17703,7 +19508,7 @@ const QWEN_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL
|
|
|
17703
19508
|
function toQwenToolName(canonical) {
|
|
17704
19509
|
return CANONICAL_TO_QWEN_TOOL_NAMES[canonical] ?? canonical;
|
|
17705
19510
|
}
|
|
17706
|
-
function toCanonicalToolName$
|
|
19511
|
+
function toCanonicalToolName$3(qwenName) {
|
|
17707
19512
|
return QWEN_TO_CANONICAL_TOOL_NAMES[qwenName] ?? qwenName;
|
|
17708
19513
|
}
|
|
17709
19514
|
function parseQwenPermissionEntry(entry, options = {}) {
|
|
@@ -17897,13 +19702,242 @@ function convertQwenToRulesyncPermissions(params) {
|
|
|
17897
19702
|
const parsed = parseQwenPermissionEntry(entry, { logger });
|
|
17898
19703
|
if (!parsed.ok) {
|
|
17899
19704
|
if (action === "deny") {
|
|
17900
|
-
const canonical = toCanonicalToolName$
|
|
19705
|
+
const canonical = toCanonicalToolName$3(parsed.toolName);
|
|
17901
19706
|
if (!permission[canonical]) permission[canonical] = {};
|
|
17902
19707
|
permission[canonical]["*"] = action;
|
|
17903
19708
|
}
|
|
17904
19709
|
continue;
|
|
17905
19710
|
}
|
|
17906
19711
|
const { toolName, pattern } = parsed;
|
|
19712
|
+
const canonical = toCanonicalToolName$3(toolName);
|
|
19713
|
+
if (!permission[canonical]) permission[canonical] = {};
|
|
19714
|
+
permission[canonical][pattern] = action;
|
|
19715
|
+
}
|
|
19716
|
+
};
|
|
19717
|
+
processEntries(params.allow, "allow");
|
|
19718
|
+
processEntries(params.ask, "ask");
|
|
19719
|
+
processEntries(params.deny, "deny");
|
|
19720
|
+
return { permission };
|
|
19721
|
+
}
|
|
19722
|
+
//#endregion
|
|
19723
|
+
//#region src/features/permissions/reasonix-permissions.ts
|
|
19724
|
+
/**
|
|
19725
|
+
* Mapping from rulesync canonical tool category names (lowercase) to Reasonix
|
|
19726
|
+
* permission-rule tool families (PascalCase).
|
|
19727
|
+
*
|
|
19728
|
+
* Reasonix's `[permissions]` rule syntax (SPEC.md §3.7) is explicitly
|
|
19729
|
+
* documented as "Claude Code-style": "Bash and file mutation approvals use
|
|
19730
|
+
* Claude Code-style families such as `Bash(npm run build)`, `Bash(npm run
|
|
19731
|
+
* test:*)`, and `Edit(docs/**)`." Reasonix also accepts legacy lowercase tool
|
|
19732
|
+
* IDs for compatibility, but new rules are saved using these PascalCase
|
|
19733
|
+
* families, so rulesync reuses the same mapping `claudecode-permissions.ts`
|
|
19734
|
+
* uses (the closest documented precedent for this syntax).
|
|
19735
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
|
|
19736
|
+
*/
|
|
19737
|
+
const CANONICAL_TO_REASONIX_TOOL_NAMES = {
|
|
19738
|
+
bash: "Bash",
|
|
19739
|
+
read: "Read",
|
|
19740
|
+
edit: "Edit",
|
|
19741
|
+
write: "Write",
|
|
19742
|
+
webfetch: "WebFetch",
|
|
19743
|
+
websearch: "WebSearch",
|
|
19744
|
+
grep: "Grep",
|
|
19745
|
+
glob: "Glob",
|
|
19746
|
+
notebookedit: "NotebookEdit",
|
|
19747
|
+
agent: "Agent"
|
|
19748
|
+
};
|
|
19749
|
+
/**
|
|
19750
|
+
* Reverse mapping from Reasonix tool names to rulesync canonical names.
|
|
19751
|
+
*/
|
|
19752
|
+
const REASONIX_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
19753
|
+
function toReasonixToolName(canonical) {
|
|
19754
|
+
return CANONICAL_TO_REASONIX_TOOL_NAMES[canonical] ?? canonical;
|
|
19755
|
+
}
|
|
19756
|
+
function toCanonicalToolName$2(reasonixName) {
|
|
19757
|
+
return REASONIX_TO_CANONICAL_TOOL_NAMES[reasonixName] ?? reasonixName;
|
|
19758
|
+
}
|
|
19759
|
+
/**
|
|
19760
|
+
* Parse a Reasonix permission entry like "Bash(npm run *)" into tool name and pattern.
|
|
19761
|
+
* If no parentheses, returns the tool name with "*" as the pattern.
|
|
19762
|
+
*/
|
|
19763
|
+
function parseReasonixPermissionEntry(entry) {
|
|
19764
|
+
const parenIndex = entry.indexOf("(");
|
|
19765
|
+
if (parenIndex === -1) return {
|
|
19766
|
+
toolName: entry,
|
|
19767
|
+
pattern: "*"
|
|
19768
|
+
};
|
|
19769
|
+
const toolName = entry.slice(0, parenIndex);
|
|
19770
|
+
if (!entry.endsWith(")")) return {
|
|
19771
|
+
toolName,
|
|
19772
|
+
pattern: "*"
|
|
19773
|
+
};
|
|
19774
|
+
return {
|
|
19775
|
+
toolName,
|
|
19776
|
+
pattern: entry.slice(parenIndex + 1, -1) || "*"
|
|
19777
|
+
};
|
|
19778
|
+
}
|
|
19779
|
+
/**
|
|
19780
|
+
* Build a Reasonix permission entry like "Bash(npm run *)".
|
|
19781
|
+
* If the pattern is "*", returns just the tool name.
|
|
19782
|
+
*/
|
|
19783
|
+
function buildReasonixPermissionEntry(toolName, pattern) {
|
|
19784
|
+
if (pattern === "*") return toolName;
|
|
19785
|
+
return `${toolName}(${pattern})`;
|
|
19786
|
+
}
|
|
19787
|
+
function parseReasonixConfig(fileContent) {
|
|
19788
|
+
const parsed = smol_toml.parse(fileContent || smol_toml.stringify({}));
|
|
19789
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
19790
|
+
return { ...parsed };
|
|
19791
|
+
}
|
|
19792
|
+
function toStringArray$1(value) {
|
|
19793
|
+
if (!Array.isArray(value)) return [];
|
|
19794
|
+
return value.filter((entry) => typeof entry === "string");
|
|
19795
|
+
}
|
|
19796
|
+
function toPermissionsTable(value) {
|
|
19797
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
19798
|
+
return { ...value };
|
|
19799
|
+
}
|
|
19800
|
+
var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
19801
|
+
toml;
|
|
19802
|
+
constructor(params) {
|
|
19803
|
+
super(params);
|
|
19804
|
+
this.toml = parseReasonixConfig(this.getFileContent());
|
|
19805
|
+
}
|
|
19806
|
+
isDeletable() {
|
|
19807
|
+
return false;
|
|
19808
|
+
}
|
|
19809
|
+
static getSettablePaths({ global } = {}) {
|
|
19810
|
+
if (global) return {
|
|
19811
|
+
relativeDirPath: REASONIX_GLOBAL_DIR,
|
|
19812
|
+
relativeFilePath: REASONIX_GLOBAL_PERMISSIONS_FILE_NAME
|
|
19813
|
+
};
|
|
19814
|
+
return {
|
|
19815
|
+
relativeDirPath: ".",
|
|
19816
|
+
relativeFilePath: REASONIX_PROJECT_PERMISSIONS_FILE_NAME
|
|
19817
|
+
};
|
|
19818
|
+
}
|
|
19819
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
19820
|
+
const paths = this.getSettablePaths({ global });
|
|
19821
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({});
|
|
19822
|
+
return new ReasonixPermissions({
|
|
19823
|
+
outputRoot,
|
|
19824
|
+
relativeDirPath: paths.relativeDirPath,
|
|
19825
|
+
relativeFilePath: paths.relativeFilePath,
|
|
19826
|
+
fileContent,
|
|
19827
|
+
validate
|
|
19828
|
+
});
|
|
19829
|
+
}
|
|
19830
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
|
|
19831
|
+
const paths = this.getSettablePaths({ global });
|
|
19832
|
+
const parsed = parseReasonixConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
|
|
19833
|
+
const config = rulesyncPermissions.getJson();
|
|
19834
|
+
const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
|
|
19835
|
+
const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
|
|
19836
|
+
const existingPermissions = toPermissionsTable(parsed.permissions);
|
|
19837
|
+
const preservedAllow = toStringArray$1(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
|
|
19838
|
+
const preservedAsk = toStringArray$1(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
|
|
19839
|
+
const preservedDeny = toStringArray$1(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
|
|
19840
|
+
if (logger && managedToolNames.has("Read")) {
|
|
19841
|
+
const droppedReadDenyEntries = toStringArray$1(existingPermissions.deny).filter((entry) => {
|
|
19842
|
+
const { toolName } = parseReasonixPermissionEntry(entry);
|
|
19843
|
+
return toolName === "Read";
|
|
19844
|
+
});
|
|
19845
|
+
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.`);
|
|
19846
|
+
}
|
|
19847
|
+
const mergedPermissions = { ...existingPermissions };
|
|
19848
|
+
const mergedAllow = (0, es_toolkit.uniq)([...preservedAllow, ...allow].toSorted());
|
|
19849
|
+
const mergedAsk = (0, es_toolkit.uniq)([...preservedAsk, ...ask].toSorted());
|
|
19850
|
+
const mergedDeny = (0, es_toolkit.uniq)([...preservedDeny, ...deny].toSorted());
|
|
19851
|
+
if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
|
|
19852
|
+
else delete mergedPermissions.allow;
|
|
19853
|
+
if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
|
|
19854
|
+
else delete mergedPermissions.ask;
|
|
19855
|
+
if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
|
|
19856
|
+
else delete mergedPermissions.deny;
|
|
19857
|
+
const merged = {
|
|
19858
|
+
...parsed,
|
|
19859
|
+
permissions: mergedPermissions
|
|
19860
|
+
};
|
|
19861
|
+
const fileContent = smol_toml.stringify(merged);
|
|
19862
|
+
return new ReasonixPermissions({
|
|
19863
|
+
outputRoot,
|
|
19864
|
+
relativeDirPath: paths.relativeDirPath,
|
|
19865
|
+
relativeFilePath: paths.relativeFilePath,
|
|
19866
|
+
fileContent,
|
|
19867
|
+
validate
|
|
19868
|
+
});
|
|
19869
|
+
}
|
|
19870
|
+
toRulesyncPermissions() {
|
|
19871
|
+
const permissions = toPermissionsTable(this.toml.permissions);
|
|
19872
|
+
const config = convertReasonixToRulesyncPermissions({
|
|
19873
|
+
allow: toStringArray$1(permissions.allow),
|
|
19874
|
+
ask: toStringArray$1(permissions.ask),
|
|
19875
|
+
deny: toStringArray$1(permissions.deny)
|
|
19876
|
+
});
|
|
19877
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
|
|
19878
|
+
}
|
|
19879
|
+
validate() {
|
|
19880
|
+
try {
|
|
19881
|
+
parseReasonixConfig(this.getFileContent());
|
|
19882
|
+
return {
|
|
19883
|
+
success: true,
|
|
19884
|
+
error: null
|
|
19885
|
+
};
|
|
19886
|
+
} catch (error) {
|
|
19887
|
+
return {
|
|
19888
|
+
success: false,
|
|
19889
|
+
error: /* @__PURE__ */ new Error(`Failed to parse Reasonix config TOML: ${formatError(error)}`)
|
|
19890
|
+
};
|
|
19891
|
+
}
|
|
19892
|
+
}
|
|
19893
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
19894
|
+
return new ReasonixPermissions({
|
|
19895
|
+
outputRoot,
|
|
19896
|
+
relativeDirPath,
|
|
19897
|
+
relativeFilePath,
|
|
19898
|
+
fileContent: smol_toml.stringify({}),
|
|
19899
|
+
validate: false
|
|
19900
|
+
});
|
|
19901
|
+
}
|
|
19902
|
+
};
|
|
19903
|
+
/**
|
|
19904
|
+
* Convert rulesync permissions config to Reasonix allow/ask/deny arrays.
|
|
19905
|
+
*/
|
|
19906
|
+
function convertRulesyncToReasonixPermissions(config) {
|
|
19907
|
+
const allow = [];
|
|
19908
|
+
const ask = [];
|
|
19909
|
+
const deny = [];
|
|
19910
|
+
for (const [category, rules] of Object.entries(config.permission)) {
|
|
19911
|
+
const reasonixToolName = toReasonixToolName(category);
|
|
19912
|
+
for (const [pattern, action] of Object.entries(rules)) {
|
|
19913
|
+
const entry = buildReasonixPermissionEntry(reasonixToolName, pattern);
|
|
19914
|
+
switch (action) {
|
|
19915
|
+
case "allow":
|
|
19916
|
+
allow.push(entry);
|
|
19917
|
+
break;
|
|
19918
|
+
case "ask":
|
|
19919
|
+
ask.push(entry);
|
|
19920
|
+
break;
|
|
19921
|
+
case "deny":
|
|
19922
|
+
deny.push(entry);
|
|
19923
|
+
break;
|
|
19924
|
+
}
|
|
19925
|
+
}
|
|
19926
|
+
}
|
|
19927
|
+
return {
|
|
19928
|
+
allow,
|
|
19929
|
+
ask,
|
|
19930
|
+
deny
|
|
19931
|
+
};
|
|
19932
|
+
}
|
|
19933
|
+
/**
|
|
19934
|
+
* Convert Reasonix allow/ask/deny arrays to rulesync permissions config.
|
|
19935
|
+
*/
|
|
19936
|
+
function convertReasonixToRulesyncPermissions(params) {
|
|
19937
|
+
const permission = {};
|
|
19938
|
+
const processEntries = (entries, action) => {
|
|
19939
|
+
for (const entry of entries) {
|
|
19940
|
+
const { toolName, pattern } = parseReasonixPermissionEntry(entry);
|
|
17907
19941
|
const canonical = toCanonicalToolName$2(toolName);
|
|
17908
19942
|
if (!permission[canonical]) permission[canonical] = {};
|
|
17909
19943
|
permission[canonical][pattern] = action;
|
|
@@ -18016,13 +20050,13 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
18016
20050
|
} catch (error) {
|
|
18017
20051
|
throw new Error(`Failed to parse existing Rovodev config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
18018
20052
|
}
|
|
18019
|
-
const config = isRecord
|
|
20053
|
+
const config = isRecord(parsed) ? { ...parsed } : {};
|
|
18020
20054
|
const toolPermissions = convertRulesyncToRovodevToolPermissions({
|
|
18021
20055
|
config: rulesyncPermissions.getJson(),
|
|
18022
20056
|
logger
|
|
18023
20057
|
});
|
|
18024
20058
|
config.toolPermissions = {
|
|
18025
|
-
...isRecord
|
|
20059
|
+
...isRecord(config.toolPermissions) ? { ...config.toolPermissions } : {},
|
|
18026
20060
|
...toolPermissions
|
|
18027
20061
|
};
|
|
18028
20062
|
return new RovodevPermissions({
|
|
@@ -18042,8 +20076,8 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
18042
20076
|
} catch (error) {
|
|
18043
20077
|
throw new Error(`Failed to parse Rovodev permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18044
20078
|
}
|
|
18045
|
-
const config = isRecord
|
|
18046
|
-
const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord
|
|
20079
|
+
const config = isRecord(parsed) ? parsed : {};
|
|
20080
|
+
const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord(config.toolPermissions) ? config.toolPermissions : {});
|
|
18047
20081
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
18048
20082
|
}
|
|
18049
20083
|
validate() {
|
|
@@ -18121,11 +20155,11 @@ function convertBashRules(rules) {
|
|
|
18121
20155
|
function convertRovodevToolPermissionsToRulesync(toolPermissions) {
|
|
18122
20156
|
const permission = {};
|
|
18123
20157
|
const bash = toolPermissions.bash;
|
|
18124
|
-
if (isRecord
|
|
20158
|
+
if (isRecord(bash)) {
|
|
18125
20159
|
const bashRules = {};
|
|
18126
20160
|
if (isPermissionAction(bash.default)) bashRules[CATCH_ALL_PATTERN$1] = bash.default;
|
|
18127
20161
|
if (Array.isArray(bash.commands)) {
|
|
18128
|
-
for (const entry of bash.commands) if (isRecord
|
|
20162
|
+
for (const entry of bash.commands) if (isRecord(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
|
|
18129
20163
|
}
|
|
18130
20164
|
if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
|
|
18131
20165
|
}
|
|
@@ -18264,21 +20298,6 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
|
18264
20298
|
}
|
|
18265
20299
|
};
|
|
18266
20300
|
/**
|
|
18267
|
-
* Parse a Takt `config.yaml` into a plain object, treating an empty file as `{}`.
|
|
18268
|
-
*/
|
|
18269
|
-
function parseTaktConfig(fileContent, relativeDirPath, relativeFilePath) {
|
|
18270
|
-
const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
|
|
18271
|
-
let parsed;
|
|
18272
|
-
try {
|
|
18273
|
-
parsed = fileContent.trim() === "" ? {} : (0, js_yaml.load)(fileContent);
|
|
18274
|
-
} catch (error) {
|
|
18275
|
-
throw new Error(`Failed to parse Takt config at ${configPath}: ${formatError(error)}`, { cause: error });
|
|
18276
|
-
}
|
|
18277
|
-
if (parsed === void 0 || parsed === null) return {};
|
|
18278
|
-
if (!isPlainObject(parsed)) throw new Error(`Failed to parse Takt config at ${configPath}: expected a YAML mapping`);
|
|
18279
|
-
return parsed;
|
|
18280
|
-
}
|
|
18281
|
-
/**
|
|
18282
20301
|
* Resolve the active Takt provider: the top-level `provider:` value, else the
|
|
18283
20302
|
* sole key in `provider_profiles`, else the `claude` default.
|
|
18284
20303
|
*/
|
|
@@ -18600,8 +20619,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
18600
20619
|
config: rulesyncPermissions.getJson(),
|
|
18601
20620
|
logger
|
|
18602
20621
|
});
|
|
18603
|
-
const agents = isRecord
|
|
18604
|
-
const profiles = isRecord
|
|
20622
|
+
const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
|
|
20623
|
+
const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
|
|
18605
20624
|
const mergedAllow = (0, es_toolkit.uniq)(allow.toSorted());
|
|
18606
20625
|
const mergedDeny = (0, es_toolkit.uniq)(deny.toSorted());
|
|
18607
20626
|
if (mergedAllow.length > 0) profiles[ALLOWLIST_KEY] = mergedAllow;
|
|
@@ -18626,8 +20645,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
18626
20645
|
} catch (error) {
|
|
18627
20646
|
throw new Error(`Failed to parse Warp permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18628
20647
|
}
|
|
18629
|
-
const agents = isRecord
|
|
18630
|
-
const profiles = isRecord
|
|
20648
|
+
const agents = isRecord(settings.agents) ? settings.agents : {};
|
|
20649
|
+
const profiles = isRecord(agents.profiles) ? agents.profiles : {};
|
|
18631
20650
|
const config = convertWarpToRulesyncPermissions({
|
|
18632
20651
|
allow: isStringArray(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
|
|
18633
20652
|
deny: isStringArray(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
|
|
@@ -18978,6 +20997,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
18978
20997
|
supportsImport: true
|
|
18979
20998
|
}
|
|
18980
20999
|
}],
|
|
21000
|
+
["devin", {
|
|
21001
|
+
class: DevinPermissions,
|
|
21002
|
+
meta: {
|
|
21003
|
+
supportsProject: true,
|
|
21004
|
+
supportsGlobal: true,
|
|
21005
|
+
supportsImport: true
|
|
21006
|
+
}
|
|
21007
|
+
}],
|
|
18981
21008
|
["factorydroid", {
|
|
18982
21009
|
class: FactorydroidPermissions,
|
|
18983
21010
|
meta: {
|
|
@@ -19010,6 +21037,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
19010
21037
|
supportsImport: true
|
|
19011
21038
|
}
|
|
19012
21039
|
}],
|
|
21040
|
+
["junie", {
|
|
21041
|
+
class: JuniePermissions,
|
|
21042
|
+
meta: {
|
|
21043
|
+
supportsProject: true,
|
|
21044
|
+
supportsGlobal: true,
|
|
21045
|
+
supportsImport: true
|
|
21046
|
+
}
|
|
21047
|
+
}],
|
|
19013
21048
|
["kilo", {
|
|
19014
21049
|
class: KiloPermissions,
|
|
19015
21050
|
meta: {
|
|
@@ -19058,6 +21093,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
19058
21093
|
supportsImport: true
|
|
19059
21094
|
}
|
|
19060
21095
|
}],
|
|
21096
|
+
["reasonix", {
|
|
21097
|
+
class: ReasonixPermissions,
|
|
21098
|
+
meta: {
|
|
21099
|
+
supportsProject: true,
|
|
21100
|
+
supportsGlobal: true,
|
|
21101
|
+
supportsImport: true
|
|
21102
|
+
}
|
|
21103
|
+
}],
|
|
19061
21104
|
["rovodev", {
|
|
19062
21105
|
class: RovodevPermissions,
|
|
19063
21106
|
meta: {
|
|
@@ -19610,7 +21653,7 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
19610
21653
|
opencode: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
19611
21654
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
19612
21655
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
19613
|
-
compatibility: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
21656
|
+
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
19614
21657
|
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
19615
21658
|
})),
|
|
19616
21659
|
kilo: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
@@ -19631,7 +21674,8 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
19631
21674
|
})),
|
|
19632
21675
|
copilotcli: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
19633
21676
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
19634
|
-
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
21677
|
+
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
21678
|
+
"argument-hint": zod_mini.z.optional(zod_mini.z.string())
|
|
19635
21679
|
})),
|
|
19636
21680
|
pi: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
19637
21681
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
@@ -21385,7 +23429,11 @@ const CopilotSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
21385
23429
|
});
|
|
21386
23430
|
/**
|
|
21387
23431
|
* Represents a GitHub Copilot skill directory.
|
|
21388
|
-
*
|
|
23432
|
+
*
|
|
23433
|
+
* Copilot discovers project skills from `.github/skills/` and personal/global
|
|
23434
|
+
* skills from `~/.copilot/skills/`. Each skill is a directory containing a
|
|
23435
|
+
* `SKILL.md` file with `name`/`description` frontmatter.
|
|
23436
|
+
* https://docs.github.com/en/copilot/concepts/agents/about-agent-skills
|
|
21389
23437
|
*/
|
|
21390
23438
|
var CopilotSkill = class CopilotSkill extends ToolSkill {
|
|
21391
23439
|
constructor({ outputRoot = process.cwd(), relativeDirPath = COPILOT_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
@@ -21407,7 +23455,7 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
|
|
|
21407
23455
|
}
|
|
21408
23456
|
}
|
|
21409
23457
|
static getSettablePaths(options) {
|
|
21410
|
-
if (options?.global)
|
|
23458
|
+
if (options?.global) return { relativeDirPath: COPILOT_SKILLS_GLOBAL_DIR_PATH };
|
|
21411
23459
|
return { relativeDirPath: COPILOT_SKILLS_DIR_PATH };
|
|
21412
23460
|
}
|
|
21413
23461
|
getFrontmatter() {
|
|
@@ -21522,7 +23570,8 @@ const CopilotcliSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
21522
23570
|
name: zod_mini.z.string(),
|
|
21523
23571
|
description: zod_mini.z.string(),
|
|
21524
23572
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
21525
|
-
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
23573
|
+
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
23574
|
+
"argument-hint": zod_mini.z.optional(zod_mini.z.string())
|
|
21526
23575
|
});
|
|
21527
23576
|
/**
|
|
21528
23577
|
* Represents a GitHub Copilot CLI skill directory.
|
|
@@ -21552,7 +23601,7 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
21552
23601
|
}
|
|
21553
23602
|
}
|
|
21554
23603
|
static getSettablePaths(options) {
|
|
21555
|
-
if (options?.global) return { relativeDirPath:
|
|
23604
|
+
if (options?.global) return { relativeDirPath: COPILOT_SKILLS_GLOBAL_DIR_PATH };
|
|
21556
23605
|
return { relativeDirPath: COPILOT_SKILLS_DIR_PATH };
|
|
21557
23606
|
}
|
|
21558
23607
|
getFrontmatter() {
|
|
@@ -21580,7 +23629,8 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
21580
23629
|
const frontmatter = this.getFrontmatter();
|
|
21581
23630
|
const copilotcliSection = {
|
|
21582
23631
|
...frontmatter.license !== void 0 && { license: frontmatter.license },
|
|
21583
|
-
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
|
|
23632
|
+
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
|
|
23633
|
+
...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] }
|
|
21584
23634
|
};
|
|
21585
23635
|
const rulesyncFrontmatter = {
|
|
21586
23636
|
name: frontmatter.name,
|
|
@@ -21606,7 +23656,8 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
21606
23656
|
name: rulesyncFrontmatter.name,
|
|
21607
23657
|
description: rulesyncFrontmatter.description,
|
|
21608
23658
|
...rulesyncFrontmatter.copilotcli?.license !== void 0 && { license: rulesyncFrontmatter.copilotcli.license },
|
|
21609
|
-
...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] }
|
|
23659
|
+
...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] },
|
|
23660
|
+
...rulesyncFrontmatter.copilotcli?.["argument-hint"] !== void 0 && { "argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"] }
|
|
21610
23661
|
};
|
|
21611
23662
|
return new CopilotcliSkill({
|
|
21612
23663
|
outputRoot,
|
|
@@ -22832,8 +24883,7 @@ var KiroSkill = class KiroSkill extends ToolSkill {
|
|
|
22832
24883
|
if (!result.success) throw result.error;
|
|
22833
24884
|
}
|
|
22834
24885
|
}
|
|
22835
|
-
static getSettablePaths(
|
|
22836
|
-
if (options?.global) throw new Error("KiroSkill does not support global mode.");
|
|
24886
|
+
static getSettablePaths(_options) {
|
|
22837
24887
|
return { relativeDirPath: KIRO_SKILLS_DIR_PATH };
|
|
22838
24888
|
}
|
|
22839
24889
|
getFrontmatter() {
|
|
@@ -22975,10 +25025,19 @@ const OpenCodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
22975
25025
|
name: zod_mini.z.string(),
|
|
22976
25026
|
description: zod_mini.z.string(),
|
|
22977
25027
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
22978
|
-
compatibility: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
25028
|
+
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
22979
25029
|
metadata: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
22980
25030
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
22981
25031
|
});
|
|
25032
|
+
/**
|
|
25033
|
+
* Reads a top-level `compatibility` value from rulesync frontmatter, accepting
|
|
25034
|
+
* both the documented string form (e.g. `compatibility: opencode`) and the
|
|
25035
|
+
* legacy object form. Returns `undefined` for any other shape.
|
|
25036
|
+
*/
|
|
25037
|
+
function readTopLevelCompatibility(value) {
|
|
25038
|
+
if (typeof value === "string") return value;
|
|
25039
|
+
if (typeof value === "object" && value !== null) return value;
|
|
25040
|
+
}
|
|
22982
25041
|
var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
22983
25042
|
constructor({ outputRoot = process.cwd(), relativeDirPath = OPENCODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
22984
25043
|
super({
|
|
@@ -23055,7 +25114,7 @@ var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
|
23055
25114
|
const opencodeSection = rulesyncFrontmatter.opencode;
|
|
23056
25115
|
const looseTopLevel = rulesyncFrontmatter;
|
|
23057
25116
|
const topLevelLicense = typeof looseTopLevel.license === "string" ? looseTopLevel.license : void 0;
|
|
23058
|
-
const topLevelCompatibility =
|
|
25117
|
+
const topLevelCompatibility = readTopLevelCompatibility(looseTopLevel.compatibility);
|
|
23059
25118
|
const topLevelMetadata = typeof looseTopLevel.metadata === "object" && looseTopLevel.metadata !== null ? looseTopLevel.metadata : void 0;
|
|
23060
25119
|
const license = opencodeSection?.license ?? topLevelLicense;
|
|
23061
25120
|
const compatibility = opencodeSection?.compatibility ?? topLevelCompatibility;
|
|
@@ -24409,7 +26468,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
24409
26468
|
meta: {
|
|
24410
26469
|
supportsProject: true,
|
|
24411
26470
|
supportsSimulated: false,
|
|
24412
|
-
supportsGlobal:
|
|
26471
|
+
supportsGlobal: true
|
|
24413
26472
|
}
|
|
24414
26473
|
}],
|
|
24415
26474
|
["copilotcli", {
|
|
@@ -24497,7 +26556,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
24497
26556
|
meta: {
|
|
24498
26557
|
supportsProject: true,
|
|
24499
26558
|
supportsSimulated: false,
|
|
24500
|
-
supportsGlobal:
|
|
26559
|
+
supportsGlobal: true
|
|
24501
26560
|
}
|
|
24502
26561
|
}],
|
|
24503
26562
|
["kiro-ide", {
|
|
@@ -24505,7 +26564,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
24505
26564
|
meta: {
|
|
24506
26565
|
supportsProject: true,
|
|
24507
26566
|
supportsSimulated: false,
|
|
24508
|
-
supportsGlobal:
|
|
26567
|
+
supportsGlobal: true
|
|
24509
26568
|
}
|
|
24510
26569
|
}],
|
|
24511
26570
|
["opencode", {
|
|
@@ -28593,7 +30652,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
28593
30652
|
class: KiroCliSubagent,
|
|
28594
30653
|
meta: {
|
|
28595
30654
|
supportsSimulated: false,
|
|
28596
|
-
supportsGlobal:
|
|
30655
|
+
supportsGlobal: true,
|
|
28597
30656
|
filePattern: "*.json"
|
|
28598
30657
|
}
|
|
28599
30658
|
}],
|
|
@@ -28601,7 +30660,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
28601
30660
|
class: KiroIdeSubagent,
|
|
28602
30661
|
meta: {
|
|
28603
30662
|
supportsSimulated: false,
|
|
28604
|
-
supportsGlobal:
|
|
30663
|
+
supportsGlobal: true,
|
|
28605
30664
|
filePattern: "*.md"
|
|
28606
30665
|
}
|
|
28607
30666
|
}],
|
|
@@ -31138,7 +33197,7 @@ const STRATEGIES = [
|
|
|
31138
33197
|
* plus companion `globs`/`description` fields. (`.devin/rules/` is the
|
|
31139
33198
|
* pre-rebrand legacy location the tool still reads.)
|
|
31140
33199
|
* - Global scope: a single plain-markdown, always-on file (no frontmatter) at
|
|
31141
|
-
* `~/.
|
|
33200
|
+
* `~/.config/devin/AGENTS.md` (Devin Local global always-on rules).
|
|
31142
33201
|
*
|
|
31143
33202
|
* Trigger inference (when no explicit devin trigger is persisted):
|
|
31144
33203
|
* - Specific globs (non wildcard) → glob
|
|
@@ -31165,8 +33224,8 @@ var DevinRule = class DevinRule extends ToolRule {
|
|
|
31165
33224
|
}
|
|
31166
33225
|
static getGlobalRootPath(excludeToolDir) {
|
|
31167
33226
|
return {
|
|
31168
|
-
relativeDirPath: buildToolPath(
|
|
31169
|
-
relativeFilePath:
|
|
33227
|
+
relativeDirPath: buildToolPath(DEVIN_GLOBAL_CONFIG_DIR_PATH, ".", excludeToolDir),
|
|
33228
|
+
relativeFilePath: DEVIN_GLOBAL_AGENTS_FILE_NAME
|
|
31170
33229
|
};
|
|
31171
33230
|
}
|
|
31172
33231
|
static getSettablePaths({ global = false, excludeToolDir } = {}) {
|
|
@@ -32056,7 +34115,7 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
|
|
|
32056
34115
|
/**
|
|
32057
34116
|
* Rule generator for Pi Coding Agent.
|
|
32058
34117
|
*
|
|
32059
|
-
* Pi loads instruction context
|
|
34118
|
+
* Pi loads instruction context from the `AGENTS.md` / `CLAUDE.md` family —
|
|
32060
34119
|
* the global `~/.pi/agent/AGENTS.md` plus files discovered by walking up the
|
|
32061
34120
|
* directory tree from the current working directory. It does NOT resolve
|
|
32062
34121
|
* `@`-imports or a TOON file list, and has no `.agents/memories/` concept, so
|
|
@@ -32067,6 +34126,15 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
|
|
|
32067
34126
|
* to map onto; their bodies are folded into the single root `AGENTS.md` by the
|
|
32068
34127
|
* RulesProcessor (there is no separate non-root output location — `nonRoot` is
|
|
32069
34128
|
* `undefined`). This mirrors the codexcli, grokcli, warp, and deepagents targets.
|
|
34129
|
+
*
|
|
34130
|
+
* Pi also loads two system-prompt instruction files that rulesync does NOT emit:
|
|
34131
|
+
* `.pi/SYSTEM.md` (global `~/.pi/agent/SYSTEM.md`) *replaces* the default system
|
|
34132
|
+
* prompt entirely, and `.pi/APPEND_SYSTEM.md` (global
|
|
34133
|
+
* `~/.pi/agent/APPEND_SYSTEM.md`) *appends* to it. rulesync's rules model only
|
|
34134
|
+
* routes a designated `root` rule to a single context file and has no convention
|
|
34135
|
+
* for marking a rule as "replace" vs "append" the system prompt, so these
|
|
34136
|
+
* surfaces are documented in docs/reference/file-formats.md and left to be
|
|
34137
|
+
* authored by hand rather than mapped to a speculative new frontmatter flag.
|
|
32070
34138
|
*/
|
|
32071
34139
|
var PiRule = class PiRule extends ToolRule {
|
|
32072
34140
|
constructor({ fileContent, root, ...rest }) {
|
|
@@ -34148,63 +36216,215 @@ function warnUnsupportedTargets(params) {
|
|
|
34148
36216
|
async function checkRulesyncDirExists(params) {
|
|
34149
36217
|
return fileExists((0, node_path.join)(params.inputRoot, RULESYNC_RELATIVE_DIR_PATH));
|
|
34150
36218
|
}
|
|
36219
|
+
function dependsOnReachable(byId, from, target) {
|
|
36220
|
+
const seen = /* @__PURE__ */ new Set();
|
|
36221
|
+
const stack = [from];
|
|
36222
|
+
while (stack.length > 0) {
|
|
36223
|
+
const current = stack.pop();
|
|
36224
|
+
if (current === void 0 || seen.has(current)) continue;
|
|
36225
|
+
seen.add(current);
|
|
36226
|
+
if (current === target) return true;
|
|
36227
|
+
for (const dep of byId.get(current)?.dependsOn ?? []) stack.push(dep);
|
|
36228
|
+
}
|
|
36229
|
+
return false;
|
|
36230
|
+
}
|
|
36231
|
+
function assertSharedFilesOrdered(steps, byId) {
|
|
36232
|
+
const writersByFile = /* @__PURE__ */ new Map();
|
|
36233
|
+
for (const step of steps) for (const file of step.writesSharedFile ?? []) writersByFile.set(file, [...writersByFile.get(file) ?? [], step.id]);
|
|
36234
|
+
for (const [file, writers] of writersByFile) for (let i = 0; i < writers.length; i++) for (let j = i + 1; j < writers.length; j++) {
|
|
36235
|
+
const a = writers[i];
|
|
36236
|
+
const b = writers[j];
|
|
36237
|
+
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.`);
|
|
36238
|
+
}
|
|
36239
|
+
}
|
|
36240
|
+
/**
|
|
36241
|
+
* Topologically sort generation steps and reject ordering hazards: a shared file
|
|
36242
|
+
* with two writers not ordered by `dependsOn` (a silent data-loss trap), an
|
|
36243
|
+
* unknown dependency, or a cycle. Reordering `steps` stays safe as a result.
|
|
36244
|
+
*
|
|
36245
|
+
* @throws Error if a shared file has unordered writers, a dependency is unknown,
|
|
36246
|
+
* or the dependency graph contains a cycle.
|
|
36247
|
+
*/
|
|
36248
|
+
function resolveExecutionOrder(steps) {
|
|
36249
|
+
const byId = new Map(steps.map((step) => [step.id, step]));
|
|
36250
|
+
assertSharedFilesOrdered(steps, byId);
|
|
36251
|
+
const unresolvedDeps = new Map(steps.map((step) => [step.id, 0]));
|
|
36252
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
36253
|
+
for (const step of steps) for (const dep of step.dependsOn ?? []) {
|
|
36254
|
+
if (!byId.has(dep)) throw new Error(`Generation step '${step.id}' depends on unknown step '${dep}'.`);
|
|
36255
|
+
unresolvedDeps.set(step.id, (unresolvedDeps.get(step.id) ?? 0) + 1);
|
|
36256
|
+
dependents.set(dep, [...dependents.get(dep) ?? [], step.id]);
|
|
36257
|
+
}
|
|
36258
|
+
const ready = steps.filter((step) => (unresolvedDeps.get(step.id) ?? 0) === 0).map((step) => step.id);
|
|
36259
|
+
const ordered = [];
|
|
36260
|
+
while (ready.length > 0) {
|
|
36261
|
+
const id = ready.shift();
|
|
36262
|
+
ordered.push(byId.get(id));
|
|
36263
|
+
for (const dependent of dependents.get(id) ?? []) {
|
|
36264
|
+
const next = (unresolvedDeps.get(dependent) ?? 0) - 1;
|
|
36265
|
+
unresolvedDeps.set(dependent, next);
|
|
36266
|
+
if (next === 0) ready.push(dependent);
|
|
36267
|
+
}
|
|
36268
|
+
}
|
|
36269
|
+
if (ordered.length !== steps.length) throw new Error("Generation steps contain a cyclic 'dependsOn' dependency.");
|
|
36270
|
+
return ordered;
|
|
36271
|
+
}
|
|
36272
|
+
/**
|
|
36273
|
+
* The static shape of the generation step graph: which steps write which shared
|
|
36274
|
+
* (read-modify-write) config files, and the `dependsOn` edges that fix a safe order
|
|
36275
|
+
* for those writers. Exported (separately from the `run` closures, which need a
|
|
36276
|
+
* live `config`/`logger`) so `resolveExecutionOrder`'s ordering guarantee can be
|
|
36277
|
+
* tested directly against the real graph rather than a hand-copied one. Readonly
|
|
36278
|
+
* so a consumer can't mutate this module-level singleton and affect every
|
|
36279
|
+
* subsequent `generate()` call in the process.
|
|
36280
|
+
*/
|
|
36281
|
+
const GENERATION_STEP_GRAPH = [
|
|
36282
|
+
{
|
|
36283
|
+
id: "ignore",
|
|
36284
|
+
writesSharedFile: ["claude-settings", "zed-settings"]
|
|
36285
|
+
},
|
|
36286
|
+
{
|
|
36287
|
+
id: "mcp",
|
|
36288
|
+
writesSharedFile: [
|
|
36289
|
+
"kilo-opencode-config",
|
|
36290
|
+
"zed-settings",
|
|
36291
|
+
"qwencode-settings",
|
|
36292
|
+
"augmentcode-settings",
|
|
36293
|
+
"hermesagent-config",
|
|
36294
|
+
"amp-settings",
|
|
36295
|
+
"codexcli-config",
|
|
36296
|
+
"grokcli-config",
|
|
36297
|
+
"vibe-config",
|
|
36298
|
+
"devin-config",
|
|
36299
|
+
"reasonix-config"
|
|
36300
|
+
],
|
|
36301
|
+
dependsOn: ["ignore"]
|
|
36302
|
+
},
|
|
36303
|
+
{ id: "commands" },
|
|
36304
|
+
{ id: "subagents" },
|
|
36305
|
+
{ id: "skills" },
|
|
36306
|
+
{
|
|
36307
|
+
id: "hooks",
|
|
36308
|
+
writesSharedFile: [
|
|
36309
|
+
"claude-settings",
|
|
36310
|
+
"qwencode-settings",
|
|
36311
|
+
"augmentcode-settings",
|
|
36312
|
+
"hermesagent-config",
|
|
36313
|
+
"kiro-agent-config",
|
|
36314
|
+
"codexcli-config",
|
|
36315
|
+
"vibe-config",
|
|
36316
|
+
"devin-config"
|
|
36317
|
+
],
|
|
36318
|
+
dependsOn: ["ignore", "mcp"]
|
|
36319
|
+
},
|
|
36320
|
+
{
|
|
36321
|
+
id: "permissions",
|
|
36322
|
+
writesSharedFile: [
|
|
36323
|
+
"claude-settings",
|
|
36324
|
+
"kilo-opencode-config",
|
|
36325
|
+
"zed-settings",
|
|
36326
|
+
"qwencode-settings",
|
|
36327
|
+
"augmentcode-settings",
|
|
36328
|
+
"hermesagent-config",
|
|
36329
|
+
"kiro-agent-config",
|
|
36330
|
+
"amp-settings",
|
|
36331
|
+
"codexcli-config",
|
|
36332
|
+
"grokcli-config",
|
|
36333
|
+
"vibe-config",
|
|
36334
|
+
"devin-config",
|
|
36335
|
+
"reasonix-config"
|
|
36336
|
+
],
|
|
36337
|
+
dependsOn: [
|
|
36338
|
+
"ignore",
|
|
36339
|
+
"hooks",
|
|
36340
|
+
"mcp"
|
|
36341
|
+
]
|
|
36342
|
+
},
|
|
36343
|
+
{
|
|
36344
|
+
id: "rules",
|
|
36345
|
+
writesSharedFile: ["kilo-opencode-config"],
|
|
36346
|
+
dependsOn: [
|
|
36347
|
+
"mcp",
|
|
36348
|
+
"skills",
|
|
36349
|
+
"permissions"
|
|
36350
|
+
]
|
|
36351
|
+
}
|
|
36352
|
+
];
|
|
34151
36353
|
/**
|
|
34152
36354
|
* Generate configuration files for AI tools.
|
|
34153
36355
|
* @throws Error if generation fails
|
|
34154
36356
|
*/
|
|
34155
36357
|
async function generate(params) {
|
|
34156
36358
|
const { config, logger } = params;
|
|
34157
|
-
|
|
34158
|
-
|
|
34159
|
-
|
|
34160
|
-
|
|
34161
|
-
|
|
34162
|
-
|
|
34163
|
-
|
|
34164
|
-
|
|
34165
|
-
|
|
34166
|
-
|
|
34167
|
-
|
|
34168
|
-
|
|
34169
|
-
|
|
34170
|
-
|
|
34171
|
-
|
|
34172
|
-
|
|
34173
|
-
|
|
34174
|
-
|
|
34175
|
-
|
|
34176
|
-
|
|
34177
|
-
|
|
34178
|
-
|
|
34179
|
-
|
|
34180
|
-
|
|
34181
|
-
|
|
34182
|
-
|
|
34183
|
-
|
|
34184
|
-
|
|
34185
|
-
|
|
34186
|
-
|
|
34187
|
-
|
|
34188
|
-
|
|
34189
|
-
|
|
34190
|
-
|
|
36359
|
+
let skillsResult;
|
|
36360
|
+
const runners = {
|
|
36361
|
+
ignore: () => generateIgnoreCore({
|
|
36362
|
+
config,
|
|
36363
|
+
logger
|
|
36364
|
+
}),
|
|
36365
|
+
mcp: () => generateMcpCore({
|
|
36366
|
+
config,
|
|
36367
|
+
logger
|
|
36368
|
+
}),
|
|
36369
|
+
commands: () => generateCommandsCore({
|
|
36370
|
+
config,
|
|
36371
|
+
logger
|
|
36372
|
+
}),
|
|
36373
|
+
subagents: () => generateSubagentsCore({
|
|
36374
|
+
config,
|
|
36375
|
+
logger
|
|
36376
|
+
}),
|
|
36377
|
+
skills: async () => {
|
|
36378
|
+
skillsResult = await generateSkillsCore({
|
|
36379
|
+
config,
|
|
36380
|
+
logger
|
|
36381
|
+
});
|
|
36382
|
+
return skillsResult;
|
|
36383
|
+
},
|
|
36384
|
+
hooks: () => generateHooksCore({
|
|
36385
|
+
config,
|
|
36386
|
+
logger
|
|
36387
|
+
}),
|
|
36388
|
+
permissions: () => generatePermissionsCore({
|
|
36389
|
+
config,
|
|
36390
|
+
logger
|
|
36391
|
+
}),
|
|
36392
|
+
rules: () => generateRulesCore({
|
|
36393
|
+
config,
|
|
36394
|
+
logger,
|
|
36395
|
+
skills: skillsResult?.skills
|
|
36396
|
+
})
|
|
36397
|
+
};
|
|
36398
|
+
const orderedSteps = resolveExecutionOrder(GENERATION_STEP_GRAPH.map((meta) => ({
|
|
36399
|
+
...meta,
|
|
36400
|
+
run: runners[meta.id]
|
|
36401
|
+
})));
|
|
36402
|
+
const resultsById = /* @__PURE__ */ new Map();
|
|
36403
|
+
for (const step of orderedSteps) resultsById.set(step.id, await step.run());
|
|
36404
|
+
if (!skillsResult) throw new Error("Skills generation step did not run.");
|
|
36405
|
+
const get = (id) => {
|
|
36406
|
+
const result = resultsById.get(id);
|
|
36407
|
+
if (!result) throw new Error(`Missing generation result for step '${id}'.`);
|
|
36408
|
+
return result;
|
|
36409
|
+
};
|
|
36410
|
+
const hasDiff = orderedSteps.some((step) => get(step.id).hasDiff);
|
|
34191
36411
|
return {
|
|
34192
|
-
rulesCount:
|
|
34193
|
-
rulesPaths:
|
|
34194
|
-
ignoreCount:
|
|
34195
|
-
ignorePaths:
|
|
34196
|
-
mcpCount:
|
|
34197
|
-
mcpPaths:
|
|
34198
|
-
commandsCount:
|
|
34199
|
-
commandsPaths:
|
|
34200
|
-
subagentsCount:
|
|
34201
|
-
subagentsPaths:
|
|
36412
|
+
rulesCount: get("rules").count,
|
|
36413
|
+
rulesPaths: get("rules").paths,
|
|
36414
|
+
ignoreCount: get("ignore").count,
|
|
36415
|
+
ignorePaths: get("ignore").paths,
|
|
36416
|
+
mcpCount: get("mcp").count,
|
|
36417
|
+
mcpPaths: get("mcp").paths,
|
|
36418
|
+
commandsCount: get("commands").count,
|
|
36419
|
+
commandsPaths: get("commands").paths,
|
|
36420
|
+
subagentsCount: get("subagents").count,
|
|
36421
|
+
subagentsPaths: get("subagents").paths,
|
|
34202
36422
|
skillsCount: skillsResult.count,
|
|
34203
36423
|
skillsPaths: skillsResult.paths,
|
|
34204
|
-
hooksCount:
|
|
34205
|
-
hooksPaths:
|
|
34206
|
-
permissionsCount:
|
|
34207
|
-
permissionsPaths:
|
|
36424
|
+
hooksCount: get("hooks").count,
|
|
36425
|
+
hooksPaths: get("hooks").paths,
|
|
36426
|
+
permissionsCount: get("permissions").count,
|
|
36427
|
+
permissionsPaths: get("permissions").paths,
|
|
34208
36428
|
skills: skillsResult.skills,
|
|
34209
36429
|
hasDiff
|
|
34210
36430
|
};
|