rulesync 9.0.2 → 9.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/cli/index.cjs +8 -2
- package/dist/cli/index.js +8 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-zmpFGK87.js → import-DywcWsgJ.js} +2763 -492
- package/dist/import-DywcWsgJ.js.map +1 -0
- package/dist/{import-BhqQwWah.cjs → import-cNqo0ZxT.cjs} +2772 -501
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +5 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-zmpFGK87.js.map +0 -1
|
@@ -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
|
/**
|
|
@@ -6518,6 +7169,12 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
|
|
|
6518
7169
|
relativeFilePath: CODEXCLI_HOOKS_FILE_NAME
|
|
6519
7170
|
};
|
|
6520
7171
|
}
|
|
7172
|
+
static getExtraSharedWritePaths() {
|
|
7173
|
+
return [{
|
|
7174
|
+
relativeDirPath: CODEXCLI_DIR,
|
|
7175
|
+
relativeFilePath: CODEXCLI_MCP_FILE_NAME
|
|
7176
|
+
}];
|
|
7177
|
+
}
|
|
6521
7178
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
6522
7179
|
const paths = CodexcliHooks.getSettablePaths({ global });
|
|
6523
7180
|
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
@@ -6750,6 +7407,20 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
|
|
|
6750
7407
|
}
|
|
6751
7408
|
};
|
|
6752
7409
|
//#endregion
|
|
7410
|
+
//#region src/utils/object.ts
|
|
7411
|
+
/**
|
|
7412
|
+
* Return a shallow copy of `obj` keeping only the entries whose value is
|
|
7413
|
+
* neither `undefined` nor `null`.
|
|
7414
|
+
*
|
|
7415
|
+
* Used to assemble generated config objects without one conditional spread per
|
|
7416
|
+
* optional field (which would otherwise exceed the lint complexity budget).
|
|
7417
|
+
*/
|
|
7418
|
+
function compact(obj) {
|
|
7419
|
+
const result = {};
|
|
7420
|
+
for (const [key, value] of Object.entries(obj)) if (value !== void 0 && value !== null) result[key] = value;
|
|
7421
|
+
return result;
|
|
7422
|
+
}
|
|
7423
|
+
//#endregion
|
|
6753
7424
|
//#region src/features/hooks/copilotcli-hooks.ts
|
|
6754
7425
|
/**
|
|
6755
7426
|
* GitHub Copilot CLI hooks.
|
|
@@ -6761,7 +7432,7 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
|
|
|
6761
7432
|
* (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
|
|
6762
7433
|
* `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
|
|
6763
7434
|
* `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
|
|
6764
|
-
* `notification`). Each entry supports three hook types:
|
|
7435
|
+
* `notification`, `preMcpToolCall`). Each entry supports three hook types:
|
|
6765
7436
|
*
|
|
6766
7437
|
* - `command` — the `bash` / `powershell` command-field shape with optional
|
|
6767
7438
|
* `timeoutSec`, plus optional `cwd` / `env`.
|
|
@@ -6874,20 +7545,24 @@ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchem
|
|
|
6874
7545
|
} else if (hookType === "http") entries.push({
|
|
6875
7546
|
type: "http",
|
|
6876
7547
|
...matcherPart,
|
|
6877
|
-
...
|
|
7548
|
+
...compact({
|
|
7549
|
+
url: def.url,
|
|
7550
|
+
headers: def.headers,
|
|
7551
|
+
allowedEnvVars: def.allowedEnvVars
|
|
7552
|
+
}),
|
|
7553
|
+
...timeoutPart,
|
|
7554
|
+
...rest
|
|
7555
|
+
});
|
|
7556
|
+
else entries.push({
|
|
7557
|
+
type: "command",
|
|
7558
|
+
...matcherPart,
|
|
7559
|
+
...compact({
|
|
7560
|
+
[commandField]: def.command,
|
|
7561
|
+
env: def.env
|
|
7562
|
+
}),
|
|
6878
7563
|
...timeoutPart,
|
|
6879
7564
|
...rest
|
|
6880
7565
|
});
|
|
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
7566
|
}
|
|
6892
7567
|
return entries;
|
|
6893
7568
|
}
|
|
@@ -7296,148 +7971,63 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
|
|
|
7296
7971
|
};
|
|
7297
7972
|
//#endregion
|
|
7298
7973
|
//#region src/features/hooks/devin-hooks.ts
|
|
7974
|
+
const DEVIN_CONVERTER_CONFIG = {
|
|
7975
|
+
supportedEvents: DEVIN_HOOK_EVENTS,
|
|
7976
|
+
canonicalToToolEventNames: CANONICAL_TO_DEVIN_EVENT_NAMES,
|
|
7977
|
+
toolToCanonicalEventNames: DEVIN_TO_CANONICAL_EVENT_NAMES,
|
|
7978
|
+
projectDirVar: "",
|
|
7979
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command", "prompt"]),
|
|
7980
|
+
noMatcherEvents: /* @__PURE__ */ new Set([
|
|
7981
|
+
"sessionStart",
|
|
7982
|
+
"sessionEnd",
|
|
7983
|
+
"stop",
|
|
7984
|
+
"beforeSubmitPrompt"
|
|
7985
|
+
])
|
|
7986
|
+
};
|
|
7299
7987
|
/**
|
|
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).
|
|
7988
|
+
* Hooks generator for Devin Local (native `.devin/` hooks).
|
|
7355
7989
|
*
|
|
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.
|
|
7990
|
+
* Devin Local adopts a Claude-Code-style lifecycle hooks model: each event maps
|
|
7991
|
+
* to an array of `{ matcher?, hooks: [{ type, command|prompt, timeout? }] }`
|
|
7992
|
+
* matcher groups.
|
|
7379
7993
|
*
|
|
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).
|
|
7994
|
+
* - Project scope: `.devin/hooks.v1.json`. This is a standalone file whose top
|
|
7995
|
+
* level IS the event map (no wrapper key).
|
|
7996
|
+
* - Global scope: `~/.config/devin/config.json` under the `"hooks"` key. This
|
|
7997
|
+
* file is shared with the MCP (`mcpServers`) and permissions (`permissions`)
|
|
7998
|
+
* features, so reads and writes merge into the existing JSON and the file is
|
|
7999
|
+
* never deleted in global mode.
|
|
7414
8000
|
*
|
|
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.
|
|
8001
|
+
* @see https://docs.devin.ai/cli/extensibility/hooks/overview
|
|
7420
8002
|
*/
|
|
7421
8003
|
var DevinHooks = class DevinHooks extends ToolHooks {
|
|
7422
8004
|
constructor(params) {
|
|
7423
8005
|
super({
|
|
7424
8006
|
...params,
|
|
7425
|
-
fileContent: params.fileContent ??
|
|
8007
|
+
fileContent: params.fileContent ?? "{}"
|
|
7426
8008
|
});
|
|
7427
8009
|
}
|
|
8010
|
+
/**
|
|
8011
|
+
* The project standalone `hooks.v1.json` is owned wholesale by rulesync and
|
|
8012
|
+
* may be deleted as an orphan. The global `config.json` is shared with the MCP
|
|
8013
|
+
* and permissions features, so it must never be deleted.
|
|
8014
|
+
*/
|
|
8015
|
+
isDeletable() {
|
|
8016
|
+
return this.getRelativeFilePath() !== DEVIN_CONFIG_FILE_NAME;
|
|
8017
|
+
}
|
|
7428
8018
|
static getSettablePaths({ global = false } = {}) {
|
|
7429
8019
|
if (global) return {
|
|
7430
|
-
relativeDirPath:
|
|
7431
|
-
relativeFilePath:
|
|
8020
|
+
relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
|
|
8021
|
+
relativeFilePath: DEVIN_CONFIG_FILE_NAME
|
|
7432
8022
|
};
|
|
7433
8023
|
return {
|
|
7434
|
-
relativeDirPath:
|
|
7435
|
-
relativeFilePath:
|
|
8024
|
+
relativeDirPath: DEVIN_DIR,
|
|
8025
|
+
relativeFilePath: DEVIN_HOOKS_V1_FILE_NAME
|
|
7436
8026
|
};
|
|
7437
8027
|
}
|
|
7438
8028
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
7439
8029
|
const paths = DevinHooks.getSettablePaths({ global });
|
|
7440
|
-
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ??
|
|
8030
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
|
|
7441
8031
|
return new DevinHooks({
|
|
7442
8032
|
outputRoot,
|
|
7443
8033
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -7448,31 +8038,32 @@ var DevinHooks = class DevinHooks extends ToolHooks {
|
|
|
7448
8038
|
}
|
|
7449
8039
|
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
7450
8040
|
const paths = DevinHooks.getSettablePaths({ global });
|
|
7451
|
-
|
|
8041
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
7452
8042
|
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);
|
|
8043
|
+
const devinHooks = canonicalToToolHooks({
|
|
8044
|
+
config,
|
|
8045
|
+
toolOverrideHooks: config.devin?.hooks,
|
|
8046
|
+
converterConfig: DEVIN_CONVERTER_CONFIG,
|
|
8047
|
+
logger
|
|
8048
|
+
});
|
|
8049
|
+
let fileContent;
|
|
8050
|
+
if (global) {
|
|
8051
|
+
const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
|
|
8052
|
+
let parsedSettings;
|
|
8053
|
+
try {
|
|
8054
|
+
parsedSettings = JSON.parse(existingContent);
|
|
8055
|
+
} catch (error) {
|
|
8056
|
+
throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
7472
8057
|
}
|
|
7473
|
-
|
|
8058
|
+
const merged = {
|
|
8059
|
+
...isRecord(parsedSettings) ? parsedSettings : {},
|
|
8060
|
+
hooks: devinHooks
|
|
8061
|
+
};
|
|
8062
|
+
fileContent = JSON.stringify(merged, null, 2);
|
|
8063
|
+
} else {
|
|
8064
|
+
await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
|
|
8065
|
+
fileContent = JSON.stringify(devinHooks, null, 2);
|
|
7474
8066
|
}
|
|
7475
|
-
const fileContent = JSON.stringify({ hooks: devinHooks }, null, 2);
|
|
7476
8067
|
return new DevinHooks({
|
|
7477
8068
|
outputRoot,
|
|
7478
8069
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -7488,22 +8079,13 @@ var DevinHooks = class DevinHooks extends ToolHooks {
|
|
|
7488
8079
|
} catch (error) {
|
|
7489
8080
|
throw new Error(`Failed to parse Devin hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
7490
8081
|
}
|
|
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
|
-
}
|
|
8082
|
+
const hooks = toolHooksToCanonical({
|
|
8083
|
+
hooks: this.getRelativeFilePath() === "config.json" ? isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {} : parsed,
|
|
8084
|
+
converterConfig: DEVIN_CONVERTER_CONFIG
|
|
8085
|
+
});
|
|
7504
8086
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
|
|
7505
8087
|
version: 1,
|
|
7506
|
-
hooks
|
|
8088
|
+
hooks
|
|
7507
8089
|
}, null, 2) });
|
|
7508
8090
|
}
|
|
7509
8091
|
validate() {
|
|
@@ -7517,7 +8099,7 @@ var DevinHooks = class DevinHooks extends ToolHooks {
|
|
|
7517
8099
|
outputRoot,
|
|
7518
8100
|
relativeDirPath,
|
|
7519
8101
|
relativeFilePath,
|
|
7520
|
-
fileContent:
|
|
8102
|
+
fileContent: "{}",
|
|
7521
8103
|
validate: false
|
|
7522
8104
|
});
|
|
7523
8105
|
}
|
|
@@ -7744,6 +8326,106 @@ function mergeHermesConfig(fileContent, patch) {
|
|
|
7744
8326
|
}
|
|
7745
8327
|
//#endregion
|
|
7746
8328
|
//#region src/features/hooks/hermesagent-hooks.ts
|
|
8329
|
+
/**
|
|
8330
|
+
* Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
|
|
8331
|
+
* `post_tool_call`) and therefore carry a `matcher`. Every other supported
|
|
8332
|
+
* canonical event maps to a Hermes lifecycle event, which never accepts a
|
|
8333
|
+
* `matcher`.
|
|
8334
|
+
* @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
|
|
8335
|
+
*/
|
|
8336
|
+
const HERMESAGENT_MATCHER_EVENTS = /* @__PURE__ */ new Set(["preToolUse", "postToolUse"]);
|
|
8337
|
+
/**
|
|
8338
|
+
* Filter the shared canonical hooks to the events Hermes understands and merge
|
|
8339
|
+
* the `hermesagent`-specific override block on top.
|
|
8340
|
+
*/
|
|
8341
|
+
function buildEffectiveHooks(config, toolOverrideHooks) {
|
|
8342
|
+
const supported = new Set(HERMESAGENT_HOOK_EVENTS);
|
|
8343
|
+
const shared = {};
|
|
8344
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (supported.has(event)) shared[event] = defs;
|
|
8345
|
+
return {
|
|
8346
|
+
...shared,
|
|
8347
|
+
...toolOverrideHooks
|
|
8348
|
+
};
|
|
8349
|
+
}
|
|
8350
|
+
/**
|
|
8351
|
+
* Convert the canonical hooks config into Hermes's native
|
|
8352
|
+
* `hooks: { <event>: [{ matcher?, command, timeout? }] }` shape.
|
|
8353
|
+
*
|
|
8354
|
+
* Only `type: "command"` canonical hooks are emitted — Hermes shell hooks run
|
|
8355
|
+
* via `shlex.split`/`shell=False`, so `prompt`/`http` hooks have no native
|
|
8356
|
+
* equivalent and are skipped (the shared `HooksProcessor` already warns about
|
|
8357
|
+
* unsupported hook types centrally). `matcher` is only carried through for
|
|
8358
|
+
* `pre_tool_call`/`post_tool_call`; on any other event it is dropped with a
|
|
8359
|
+
* warning, mirroring how other adapters (e.g. AugmentCode) handle
|
|
8360
|
+
* matcher-less lifecycle events.
|
|
8361
|
+
*/
|
|
8362
|
+
function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
|
|
8363
|
+
const effectiveHooks = buildEffectiveHooks(config, toolOverrideHooks);
|
|
8364
|
+
const result = {};
|
|
8365
|
+
for (const [canonicalEvent, defs] of Object.entries(effectiveHooks)) {
|
|
8366
|
+
const nativeEvent = CANONICAL_TO_HERMESAGENT_EVENT_NAMES[canonicalEvent];
|
|
8367
|
+
if (!nativeEvent) continue;
|
|
8368
|
+
const supportsMatcher = HERMESAGENT_MATCHER_EVENTS.has(canonicalEvent);
|
|
8369
|
+
const entries = [];
|
|
8370
|
+
for (const def of defs) {
|
|
8371
|
+
if ((def.type ?? "command") !== "command") continue;
|
|
8372
|
+
if (typeof def.command !== "string" || def.command === "") continue;
|
|
8373
|
+
const entry = { command: def.command };
|
|
8374
|
+
if (typeof def.matcher === "string" && def.matcher !== "") if (supportsMatcher) entry.matcher = def.matcher;
|
|
8375
|
+
else logger?.warn(`matcher "${def.matcher}" on "${canonicalEvent}" hook will be ignored — Hermes Agent only supports matchers on pre_tool_call/post_tool_call`);
|
|
8376
|
+
if (typeof def.timeout === "number") entry.timeout = def.timeout;
|
|
8377
|
+
entries.push(entry);
|
|
8378
|
+
}
|
|
8379
|
+
if (entries.length > 0) result[nativeEvent] = entries;
|
|
8380
|
+
}
|
|
8381
|
+
return result;
|
|
8382
|
+
}
|
|
8383
|
+
/**
|
|
8384
|
+
* Reverse {@link canonicalToHermesHooks}: parse Hermes's native
|
|
8385
|
+
* `hooks: { <event>: [...] }` map back into a canonical event → definition[]
|
|
8386
|
+
* record. Native events with no canonical equivalent (`pre_verify`,
|
|
8387
|
+
* `transform_tool_result`, ...) are skipped since there is nothing to round
|
|
8388
|
+
* -trip them into.
|
|
8389
|
+
*/
|
|
8390
|
+
function hermesHooksToCanonical(hooks) {
|
|
8391
|
+
const canonical = {};
|
|
8392
|
+
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
|
|
8393
|
+
for (const [nativeEvent, entries] of Object.entries(hooks)) {
|
|
8394
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(nativeEvent) || !Array.isArray(entries)) continue;
|
|
8395
|
+
const canonicalEvent = HERMESAGENT_TO_CANONICAL_EVENT_NAMES[nativeEvent];
|
|
8396
|
+
if (!canonicalEvent) continue;
|
|
8397
|
+
const defs = [];
|
|
8398
|
+
for (const raw of entries) {
|
|
8399
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
8400
|
+
const entry = raw;
|
|
8401
|
+
if (typeof entry.command !== "string") continue;
|
|
8402
|
+
const def = {
|
|
8403
|
+
type: "command",
|
|
8404
|
+
command: entry.command
|
|
8405
|
+
};
|
|
8406
|
+
if (typeof entry.matcher === "string" && entry.matcher !== "") def.matcher = entry.matcher;
|
|
8407
|
+
if (typeof entry.timeout === "number") def.timeout = entry.timeout;
|
|
8408
|
+
defs.push(def);
|
|
8409
|
+
}
|
|
8410
|
+
if (defs.length > 0) canonical[canonicalEvent] = defs;
|
|
8411
|
+
}
|
|
8412
|
+
return canonical;
|
|
8413
|
+
}
|
|
8414
|
+
/**
|
|
8415
|
+
* Hermes Agent shell hooks.
|
|
8416
|
+
*
|
|
8417
|
+
* Hermes Agent registers shell-command hooks under the `hooks:` key of the
|
|
8418
|
+
* shared user config file `~/.hermes/config.yaml` (the HERMES_HOME directory;
|
|
8419
|
+
* global only — Hermes has no project-scoped hooks location). Hermes only runs
|
|
8420
|
+
* hooks declared under its fixed `VALID_HOOKS` event keys (`pre_tool_call`,
|
|
8421
|
+
* `post_tool_call`, `pre_llm_call`, `post_llm_call`, `on_session_start`,
|
|
8422
|
+
* `on_session_end`, `subagent_start`, `subagent_stop`, ...); any other key is
|
|
8423
|
+
* silently ignored. Generation therefore maps canonical events onto the real
|
|
8424
|
+
* `VALID_HOOKS` keys and merges the resulting `hooks:` block into the existing
|
|
8425
|
+
* config instead of overwriting it, since that file also holds other Hermes
|
|
8426
|
+
* settings (model, `mcp_servers`, `command_allowlist`, ...).
|
|
8427
|
+
* @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
|
|
8428
|
+
*/
|
|
7747
8429
|
var HermesagentHooks = class HermesagentHooks extends ToolHooks {
|
|
7748
8430
|
static getSettablePaths() {
|
|
7749
8431
|
return {
|
|
@@ -7763,6 +8445,9 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
|
|
|
7763
8445
|
error: null
|
|
7764
8446
|
};
|
|
7765
8447
|
}
|
|
8448
|
+
isDeletable() {
|
|
8449
|
+
return false;
|
|
8450
|
+
}
|
|
7766
8451
|
shouldMergeExistingFileContent() {
|
|
7767
8452
|
return true;
|
|
7768
8453
|
}
|
|
@@ -7770,18 +8455,21 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
|
|
|
7770
8455
|
this.fileContent = mergeHermesConfig(fileContent, parseHermesConfig(this.fileContent));
|
|
7771
8456
|
}
|
|
7772
8457
|
toRulesyncHooks() {
|
|
7773
|
-
const
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
fileContent: JSON.stringify(hooks ?? {}, null, 2)
|
|
7779
|
-
});
|
|
8458
|
+
const hooks = hermesHooksToCanonical(parseHermesConfig(this.getFileContent()).hooks);
|
|
8459
|
+
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
|
|
8460
|
+
version: 1,
|
|
8461
|
+
hooks
|
|
8462
|
+
}, null, 2) });
|
|
7780
8463
|
}
|
|
7781
|
-
static fromRulesyncHooks({ outputRoot, rulesyncHooks }) {
|
|
8464
|
+
static fromRulesyncHooks({ outputRoot, rulesyncHooks, logger }) {
|
|
8465
|
+
const config = rulesyncHooks.getJson();
|
|
7782
8466
|
return new HermesagentHooks({
|
|
7783
8467
|
outputRoot,
|
|
7784
|
-
fileContent: stringifyHermesConfig({ hooks: {
|
|
8468
|
+
fileContent: stringifyHermesConfig({ hooks: canonicalToHermesHooks({
|
|
8469
|
+
config,
|
|
8470
|
+
toolOverrideHooks: config.hermesagent?.hooks,
|
|
8471
|
+
logger
|
|
8472
|
+
}) })
|
|
7785
8473
|
});
|
|
7786
8474
|
}
|
|
7787
8475
|
};
|
|
@@ -8251,9 +8939,8 @@ var KiroHooks = class KiroHooks extends ToolHooks {
|
|
|
8251
8939
|
* the tool-specific override key to `kiro-cli` (so `kiro-cli.hooks` overrides in
|
|
8252
8940
|
* the rulesync hooks config are honored, rather than the legacy `kiro.hooks`).
|
|
8253
8941
|
*
|
|
8254
|
-
* (The Kiro IDE uses
|
|
8255
|
-
*
|
|
8256
|
-
* adapter.)
|
|
8942
|
+
* (The Kiro IDE uses the structured `.kiro/hooks/*.json` v1 format instead; see
|
|
8943
|
+
* {@link import("./kiro-ide-hooks.js").KiroIdeHooks}.)
|
|
8257
8944
|
*/
|
|
8258
8945
|
var KiroCliHooks = class extends KiroHooks {
|
|
8259
8946
|
static getOverrideKey() {
|
|
@@ -8261,6 +8948,203 @@ var KiroCliHooks = class extends KiroHooks {
|
|
|
8261
8948
|
}
|
|
8262
8949
|
};
|
|
8263
8950
|
//#endregion
|
|
8951
|
+
//#region src/features/hooks/kiro-ide-hooks.ts
|
|
8952
|
+
/**
|
|
8953
|
+
* One hook entry inside the Kiro IDE v1 `hooks` array.
|
|
8954
|
+
*
|
|
8955
|
+
* `z.looseObject` keeps unknown fields added by future Kiro IDE versions, so
|
|
8956
|
+
* imports do not drop data they do not yet understand.
|
|
8957
|
+
* @see https://kiro.dev/docs/hooks/types/
|
|
8958
|
+
*/
|
|
8959
|
+
const KiroIdeHookActionSchema = zod_mini.z.union([zod_mini.z.looseObject({
|
|
8960
|
+
type: zod_mini.z.literal("command"),
|
|
8961
|
+
command: zod_mini.z.optional(safeString)
|
|
8962
|
+
}), zod_mini.z.looseObject({
|
|
8963
|
+
type: zod_mini.z.literal("agent"),
|
|
8964
|
+
prompt: zod_mini.z.optional(safeString)
|
|
8965
|
+
})]);
|
|
8966
|
+
const KiroIdeHookEntrySchema = zod_mini.z.looseObject({
|
|
8967
|
+
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
8968
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
8969
|
+
trigger: zod_mini.z.optional(zod_mini.z.string()),
|
|
8970
|
+
matcher: zod_mini.z.optional(zod_mini.z.string()),
|
|
8971
|
+
action: zod_mini.z.optional(KiroIdeHookActionSchema),
|
|
8972
|
+
timeout: zod_mini.z.optional(zod_mini.z.number()),
|
|
8973
|
+
enabled: zod_mini.z.optional(zod_mini.z.boolean())
|
|
8974
|
+
});
|
|
8975
|
+
const KiroIdeHooksFileSchema = zod_mini.z.looseObject({
|
|
8976
|
+
version: zod_mini.z.optional(zod_mini.z.string()),
|
|
8977
|
+
hooks: zod_mini.z.optional(zod_mini.z.array(KiroIdeHookEntrySchema))
|
|
8978
|
+
});
|
|
8979
|
+
/**
|
|
8980
|
+
* Build the Kiro IDE hook entries for a single canonical event's definitions.
|
|
8981
|
+
*
|
|
8982
|
+
* `command`-type definitions become `{ type: "command", command }` actions and
|
|
8983
|
+
* `prompt`-type definitions become `{ type: "agent", prompt }` actions. Other
|
|
8984
|
+
* types are skipped (the {@link import("./hooks-processor.js").HooksProcessor}
|
|
8985
|
+
* already warns about unsupported types).
|
|
8986
|
+
*/
|
|
8987
|
+
function buildKiroIdeEntriesForEvent(trigger, definitions) {
|
|
8988
|
+
const entries = [];
|
|
8989
|
+
for (const def of definitions) {
|
|
8990
|
+
const type = def.type ?? "command";
|
|
8991
|
+
let action;
|
|
8992
|
+
if (type === "command") {
|
|
8993
|
+
if (def.command === void 0) continue;
|
|
8994
|
+
action = {
|
|
8995
|
+
type: "command",
|
|
8996
|
+
command: def.command
|
|
8997
|
+
};
|
|
8998
|
+
} else if (type === "prompt") {
|
|
8999
|
+
if (def.prompt === void 0) continue;
|
|
9000
|
+
action = {
|
|
9001
|
+
type: "agent",
|
|
9002
|
+
prompt: def.prompt
|
|
9003
|
+
};
|
|
9004
|
+
} else continue;
|
|
9005
|
+
entries.push({
|
|
9006
|
+
name: def.name ?? trigger,
|
|
9007
|
+
...def.description !== void 0 && def.description !== null && { description: def.description },
|
|
9008
|
+
trigger,
|
|
9009
|
+
...def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" && { matcher: def.matcher },
|
|
9010
|
+
action,
|
|
9011
|
+
...def.timeout !== void 0 && def.timeout !== null && def.timeout >= 0 && { timeout: def.timeout },
|
|
9012
|
+
enabled: true
|
|
9013
|
+
});
|
|
9014
|
+
}
|
|
9015
|
+
return entries;
|
|
9016
|
+
}
|
|
9017
|
+
function canonicalToKiroIdeHooks(config) {
|
|
9018
|
+
const kiroIdeSupported = new Set(KIRO_IDE_HOOK_EVENTS);
|
|
9019
|
+
const sharedHooks = {};
|
|
9020
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (kiroIdeSupported.has(event)) sharedHooks[event] = defs;
|
|
9021
|
+
const effectiveHooks = {
|
|
9022
|
+
...sharedHooks,
|
|
9023
|
+
...config["kiro-ide"]?.hooks
|
|
9024
|
+
};
|
|
9025
|
+
const entries = [];
|
|
9026
|
+
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
9027
|
+
const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? eventName;
|
|
9028
|
+
entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
|
|
9029
|
+
}
|
|
9030
|
+
return entries;
|
|
9031
|
+
}
|
|
9032
|
+
function kiroIdeHooksToCanonical(entries) {
|
|
9033
|
+
const canonical = {};
|
|
9034
|
+
for (const entry of entries) {
|
|
9035
|
+
if (entry.trigger === void 0 || entry.action === void 0) continue;
|
|
9036
|
+
const eventName = KIRO_IDE_TO_CANONICAL_EVENT_NAMES[entry.trigger] ?? entry.trigger;
|
|
9037
|
+
if (isPrototypePollutionKey(eventName)) continue;
|
|
9038
|
+
const def = {};
|
|
9039
|
+
if (entry.action.type === "command") {
|
|
9040
|
+
if (!entry.action.command) continue;
|
|
9041
|
+
def.type = "command";
|
|
9042
|
+
def.command = entry.action.command;
|
|
9043
|
+
} else {
|
|
9044
|
+
if (!entry.action.prompt) continue;
|
|
9045
|
+
def.type = "prompt";
|
|
9046
|
+
def.prompt = entry.action.prompt;
|
|
9047
|
+
}
|
|
9048
|
+
if (entry.name !== void 0 && entry.name !== null) def.name = entry.name;
|
|
9049
|
+
if (entry.description !== void 0 && entry.description !== null) def.description = entry.description;
|
|
9050
|
+
if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
|
|
9051
|
+
if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
|
|
9052
|
+
(canonical[eventName] ??= []).push(def);
|
|
9053
|
+
}
|
|
9054
|
+
return canonical;
|
|
9055
|
+
}
|
|
9056
|
+
/**
|
|
9057
|
+
* Hooks generator for the **Kiro IDE** (`.kiro/hooks/*.json` v1).
|
|
9058
|
+
*
|
|
9059
|
+
* Kiro IDE 1.0 reads structured JSON hooks from `.kiro/hooks/` (workspace) and
|
|
9060
|
+
* `~/.kiro/hooks/` (user). A single file may declare multiple hooks in its
|
|
9061
|
+
* `hooks` array, so rulesync emits every generated hook into one
|
|
9062
|
+
* `rulesync.json` file per scope (`{ "version": "v1", "hooks": [ ... ] }`),
|
|
9063
|
+
* which keeps it within the single-file hooks architecture.
|
|
9064
|
+
*
|
|
9065
|
+
* This is distinct from the Kiro CLI ({@link import("./kiro-cli-hooks.js").
|
|
9066
|
+
* KiroCliHooks}), which uses the `.kiro/agents/default.json` agent-config shape.
|
|
9067
|
+
*
|
|
9068
|
+
* @see https://kiro.dev/docs/hooks/
|
|
9069
|
+
*/
|
|
9070
|
+
var KiroIdeHooks = class KiroIdeHooks extends ToolHooks {
|
|
9071
|
+
constructor(params) {
|
|
9072
|
+
super({
|
|
9073
|
+
...params,
|
|
9074
|
+
fileContent: params.fileContent ?? JSON.stringify({
|
|
9075
|
+
version: "v1",
|
|
9076
|
+
hooks: []
|
|
9077
|
+
}, null, 2)
|
|
9078
|
+
});
|
|
9079
|
+
}
|
|
9080
|
+
static getSettablePaths(_options = {}) {
|
|
9081
|
+
return {
|
|
9082
|
+
relativeDirPath: KIRO_IDE_HOOKS_DIR_PATH,
|
|
9083
|
+
relativeFilePath: KIRO_IDE_HOOKS_FILE_NAME
|
|
9084
|
+
};
|
|
9085
|
+
}
|
|
9086
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
9087
|
+
const paths = KiroIdeHooks.getSettablePaths({ global });
|
|
9088
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({
|
|
9089
|
+
version: "v1",
|
|
9090
|
+
hooks: []
|
|
9091
|
+
}, null, 2);
|
|
9092
|
+
return new KiroIdeHooks({
|
|
9093
|
+
outputRoot,
|
|
9094
|
+
relativeDirPath: paths.relativeDirPath,
|
|
9095
|
+
relativeFilePath: paths.relativeFilePath,
|
|
9096
|
+
fileContent,
|
|
9097
|
+
validate
|
|
9098
|
+
});
|
|
9099
|
+
}
|
|
9100
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
|
|
9101
|
+
const paths = KiroIdeHooks.getSettablePaths({ global });
|
|
9102
|
+
const hooks = canonicalToKiroIdeHooks(rulesyncHooks.getJson());
|
|
9103
|
+
const fileContent = JSON.stringify({
|
|
9104
|
+
version: "v1",
|
|
9105
|
+
hooks
|
|
9106
|
+
}, null, 2);
|
|
9107
|
+
return new KiroIdeHooks({
|
|
9108
|
+
outputRoot,
|
|
9109
|
+
relativeDirPath: paths.relativeDirPath,
|
|
9110
|
+
relativeFilePath: paths.relativeFilePath,
|
|
9111
|
+
fileContent,
|
|
9112
|
+
validate
|
|
9113
|
+
});
|
|
9114
|
+
}
|
|
9115
|
+
toRulesyncHooks() {
|
|
9116
|
+
let parsed;
|
|
9117
|
+
try {
|
|
9118
|
+
parsed = KiroIdeHooksFileSchema.parse(JSON.parse(this.getFileContent()));
|
|
9119
|
+
} catch (error) {
|
|
9120
|
+
throw new Error(`Failed to parse Kiro IDE hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
9121
|
+
}
|
|
9122
|
+
const hooks = kiroIdeHooksToCanonical(parsed.hooks ?? []);
|
|
9123
|
+
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
|
|
9124
|
+
version: 1,
|
|
9125
|
+
hooks
|
|
9126
|
+
}, null, 2) });
|
|
9127
|
+
}
|
|
9128
|
+
validate() {
|
|
9129
|
+
return {
|
|
9130
|
+
success: true,
|
|
9131
|
+
error: null
|
|
9132
|
+
};
|
|
9133
|
+
}
|
|
9134
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
9135
|
+
return new KiroIdeHooks({
|
|
9136
|
+
outputRoot,
|
|
9137
|
+
relativeDirPath,
|
|
9138
|
+
relativeFilePath,
|
|
9139
|
+
fileContent: JSON.stringify({
|
|
9140
|
+
version: "v1",
|
|
9141
|
+
hooks: []
|
|
9142
|
+
}, null, 2),
|
|
9143
|
+
validate: false
|
|
9144
|
+
});
|
|
9145
|
+
}
|
|
9146
|
+
};
|
|
9147
|
+
//#endregion
|
|
8264
9148
|
//#region src/features/hooks/opencode-hooks.ts
|
|
8265
9149
|
var OpencodeHooks = class OpencodeHooks extends ToolHooks {
|
|
8266
9150
|
constructor(params) {
|
|
@@ -8319,6 +9203,35 @@ var OpencodeHooks = class OpencodeHooks extends ToolHooks {
|
|
|
8319
9203
|
//#endregion
|
|
8320
9204
|
//#region src/features/hooks/qwencode-hooks.ts
|
|
8321
9205
|
/**
|
|
9206
|
+
* Build a single Qwen Code hook object from a canonical hook definition.
|
|
9207
|
+
* Command-only fields (`async`/`env`/`shell`) are emitted only on command hooks
|
|
9208
|
+
* and http-only fields (`headers`/`allowedEnvVars`/`once`) only on http hooks,
|
|
9209
|
+
* matching upstream. `statusMessage` applies to both.
|
|
9210
|
+
* https://github.com/QwenLM/qwen-code/blob/main/docs/users/features/hooks.md
|
|
9211
|
+
*/
|
|
9212
|
+
function canonicalDefToQwencodeHook(def) {
|
|
9213
|
+
const type = def.type ?? "command";
|
|
9214
|
+
const isHttp = type === "http";
|
|
9215
|
+
const isCommand = type === "command";
|
|
9216
|
+
return {
|
|
9217
|
+
type,
|
|
9218
|
+
...compact({
|
|
9219
|
+
command: def.command,
|
|
9220
|
+
url: def.url,
|
|
9221
|
+
timeout: def.timeout,
|
|
9222
|
+
name: def.name,
|
|
9223
|
+
description: def.description,
|
|
9224
|
+
statusMessage: def.statusMessage,
|
|
9225
|
+
async: isCommand ? def.async : void 0,
|
|
9226
|
+
env: isCommand ? def.env : void 0,
|
|
9227
|
+
shell: isCommand ? def.shell : void 0,
|
|
9228
|
+
headers: isHttp ? def.headers : void 0,
|
|
9229
|
+
allowedEnvVars: isHttp ? def.allowedEnvVars : void 0,
|
|
9230
|
+
once: isHttp ? def.once : void 0
|
|
9231
|
+
})
|
|
9232
|
+
};
|
|
9233
|
+
}
|
|
9234
|
+
/**
|
|
8322
9235
|
* Convert canonical hooks config to Qwen Code format.
|
|
8323
9236
|
* Filters shared hooks to QWENCODE_HOOK_EVENTS, merges config.qwencode?.hooks,
|
|
8324
9237
|
* then converts to PascalCase and Qwen Code matcher/hooks structure.
|
|
@@ -8346,16 +9259,7 @@ function canonicalToQwencodeHooks(config) {
|
|
|
8346
9259
|
}
|
|
8347
9260
|
const entries = [];
|
|
8348
9261
|
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
|
-
});
|
|
9262
|
+
const hooks = defs.map(canonicalDefToQwencodeHook);
|
|
8359
9263
|
const sequential = defs.some((def) => def.sequential === true);
|
|
8360
9264
|
const group = matcherKey ? {
|
|
8361
9265
|
matcher: matcherKey,
|
|
@@ -8379,7 +9283,14 @@ const QwencodeHookEntrySchema = zod_mini.z.looseObject({
|
|
|
8379
9283
|
url: zod_mini.z.optional(zod_mini.z.string()),
|
|
8380
9284
|
timeout: zod_mini.z.optional(zod_mini.z.number()),
|
|
8381
9285
|
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
8382
|
-
description: zod_mini.z.optional(zod_mini.z.string())
|
|
9286
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
9287
|
+
statusMessage: zod_mini.z.optional(zod_mini.z.string()),
|
|
9288
|
+
async: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
9289
|
+
env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
9290
|
+
shell: zod_mini.z.optional(zod_mini.z.string()),
|
|
9291
|
+
headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
9292
|
+
allowedEnvVars: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
9293
|
+
once: zod_mini.z.optional(zod_mini.z.boolean())
|
|
8383
9294
|
});
|
|
8384
9295
|
/**
|
|
8385
9296
|
* A matcher group entry in a Qwen Code event array.
|
|
@@ -8398,18 +9309,29 @@ function qwencodeMatcherEntryToCanonical(entry) {
|
|
|
8398
9309
|
const defs = [];
|
|
8399
9310
|
const hooks = entry.hooks ?? [];
|
|
8400
9311
|
const sequential = entry.sequential === true;
|
|
9312
|
+
const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" ? entry.matcher : void 0;
|
|
8401
9313
|
for (const h of hooks) {
|
|
8402
|
-
const command = h.command;
|
|
8403
9314
|
const hookType = h.type === "command" || h.type === "prompt" || h.type === "http" ? h.type : "command";
|
|
9315
|
+
const isHttp = hookType === "http";
|
|
9316
|
+
const isCommand = hookType === "command";
|
|
8404
9317
|
defs.push({
|
|
8405
9318
|
type: hookType,
|
|
8406
|
-
...
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
9319
|
+
...compact({
|
|
9320
|
+
command: h.command,
|
|
9321
|
+
url: h.url,
|
|
9322
|
+
timeout: h.timeout,
|
|
9323
|
+
name: h.name,
|
|
9324
|
+
description: h.description,
|
|
9325
|
+
statusMessage: h.statusMessage,
|
|
9326
|
+
async: isCommand ? h.async : void 0,
|
|
9327
|
+
env: isCommand ? h.env : void 0,
|
|
9328
|
+
shell: isCommand ? h.shell : void 0,
|
|
9329
|
+
headers: isHttp ? h.headers : void 0,
|
|
9330
|
+
allowedEnvVars: isHttp ? h.allowedEnvVars : void 0,
|
|
9331
|
+
once: isHttp ? h.once : void 0,
|
|
9332
|
+
sequential: sequential ? true : void 0,
|
|
9333
|
+
matcher
|
|
9334
|
+
})
|
|
8413
9335
|
});
|
|
8414
9336
|
}
|
|
8415
9337
|
return defs;
|
|
@@ -8522,6 +9444,169 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
|
|
|
8522
9444
|
}
|
|
8523
9445
|
};
|
|
8524
9446
|
//#endregion
|
|
9447
|
+
//#region src/features/hooks/reasonix-hooks.ts
|
|
9448
|
+
/**
|
|
9449
|
+
* Only PreToolUse/PostToolUse honor the `match` field (an anchored regex
|
|
9450
|
+
* against the tool name); it is ignored on every other event.
|
|
9451
|
+
*/
|
|
9452
|
+
const REASONIX_MATCHER_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse"]);
|
|
9453
|
+
const SUPPORTED_REASONIX_EVENTS = new Set(REASONIX_HOOK_EVENTS);
|
|
9454
|
+
/**
|
|
9455
|
+
* Convert canonical hooks config to the Reasonix `hooks` object.
|
|
9456
|
+
* Filters shared hooks to REASONIX_HOOK_EVENTS, merges `config.reasonix?.hooks`,
|
|
9457
|
+
* then maps event names and emits flat per-event hook-entry arrays.
|
|
9458
|
+
*/
|
|
9459
|
+
function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
|
|
9460
|
+
const sharedHooks = {};
|
|
9461
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_REASONIX_EVENTS.has(event)) sharedHooks[event] = defs;
|
|
9462
|
+
const effectiveHooks = {
|
|
9463
|
+
...sharedHooks,
|
|
9464
|
+
...toolOverrideHooks
|
|
9465
|
+
};
|
|
9466
|
+
const result = {};
|
|
9467
|
+
for (const [event, defs] of Object.entries(effectiveHooks)) {
|
|
9468
|
+
if (!SUPPORTED_REASONIX_EVENTS.has(event)) continue;
|
|
9469
|
+
const reasonixEvent = CANONICAL_TO_REASONIX_EVENT_NAMES[event] ?? event;
|
|
9470
|
+
const isMatcherEvent = REASONIX_MATCHER_EVENTS.has(reasonixEvent);
|
|
9471
|
+
const entries = [];
|
|
9472
|
+
for (const def of defs) {
|
|
9473
|
+
if ((def.type ?? "command") !== "command") continue;
|
|
9474
|
+
if (typeof def.command !== "string") continue;
|
|
9475
|
+
const entry = { command: def.command };
|
|
9476
|
+
if (typeof def.matcher === "string" && def.matcher !== "") if (isMatcherEvent) entry.match = def.matcher;
|
|
9477
|
+
else logger?.warn(`matcher "${def.matcher}" on "${event}" hook will be ignored — Reasonix's "${reasonixEvent}" event does not support matchers`);
|
|
9478
|
+
if (typeof def.description === "string" && def.description !== "") entry.description = def.description;
|
|
9479
|
+
if (typeof def.timeout === "number") entry.timeout = Math.round(def.timeout * 1e3);
|
|
9480
|
+
entries.push(entry);
|
|
9481
|
+
}
|
|
9482
|
+
if (entries.length > 0) result[reasonixEvent] = [...result[reasonixEvent] ?? [], ...entries];
|
|
9483
|
+
}
|
|
9484
|
+
return result;
|
|
9485
|
+
}
|
|
9486
|
+
/**
|
|
9487
|
+
* Reverse {@link canonicalToReasonixHooks}: parse the Reasonix `hooks` object
|
|
9488
|
+
* back into a canonical event -> definition[] record.
|
|
9489
|
+
*/
|
|
9490
|
+
function reasonixHooksToCanonical(hooks) {
|
|
9491
|
+
const canonical = {};
|
|
9492
|
+
if (hooks === null || hooks === void 0 || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
|
|
9493
|
+
for (const [reasonixEvent, rawEntries] of Object.entries(hooks)) {
|
|
9494
|
+
if (!Array.isArray(rawEntries)) continue;
|
|
9495
|
+
const canonicalEvent = REASONIX_TO_CANONICAL_EVENT_NAMES[reasonixEvent] ?? reasonixEvent;
|
|
9496
|
+
const defs = [];
|
|
9497
|
+
for (const rawEntry of rawEntries) {
|
|
9498
|
+
if (rawEntry === null || typeof rawEntry !== "object" || Array.isArray(rawEntry)) continue;
|
|
9499
|
+
const entry = rawEntry;
|
|
9500
|
+
if (typeof entry.command !== "string") continue;
|
|
9501
|
+
const def = {
|
|
9502
|
+
type: "command",
|
|
9503
|
+
command: entry.command
|
|
9504
|
+
};
|
|
9505
|
+
if (typeof entry.match === "string" && entry.match !== "") def.matcher = entry.match;
|
|
9506
|
+
if (typeof entry.description === "string" && entry.description !== "") def.description = entry.description;
|
|
9507
|
+
if (typeof entry.timeout === "number") def.timeout = entry.timeout / 1e3;
|
|
9508
|
+
defs.push(def);
|
|
9509
|
+
}
|
|
9510
|
+
if (defs.length > 0) canonical[canonicalEvent] = [...canonical[canonicalEvent] ?? [], ...defs];
|
|
9511
|
+
}
|
|
9512
|
+
return canonical;
|
|
9513
|
+
}
|
|
9514
|
+
/**
|
|
9515
|
+
* Reasonix hooks adapter.
|
|
9516
|
+
*
|
|
9517
|
+
* Reasonix hooks live in a Claude-Code-style but standalone JSON file —
|
|
9518
|
+
* `.reasonix/settings.json` (project) or `~/.reasonix/settings.json`
|
|
9519
|
+
* (global) — separate from the `[permissions]`/`[[plugins]]` TOML config.
|
|
9520
|
+
* Only the four events documented in the upstream issue are mapped:
|
|
9521
|
+
* PreToolUse/PostToolUse/UserPromptSubmit/Stop (see REASONIX_HOOK_EVENTS).
|
|
9522
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
|
|
9523
|
+
*/
|
|
9524
|
+
var ReasonixHooks = class ReasonixHooks extends ToolHooks {
|
|
9525
|
+
constructor(params) {
|
|
9526
|
+
super({
|
|
9527
|
+
...params,
|
|
9528
|
+
fileContent: params.fileContent ?? "{}"
|
|
9529
|
+
});
|
|
9530
|
+
}
|
|
9531
|
+
isDeletable() {
|
|
9532
|
+
return false;
|
|
9533
|
+
}
|
|
9534
|
+
static getSettablePaths(_options = {}) {
|
|
9535
|
+
return {
|
|
9536
|
+
relativeDirPath: REASONIX_DIR,
|
|
9537
|
+
relativeFilePath: REASONIX_SETTINGS_FILE_NAME
|
|
9538
|
+
};
|
|
9539
|
+
}
|
|
9540
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
9541
|
+
const paths = ReasonixHooks.getSettablePaths({ global });
|
|
9542
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
|
|
9543
|
+
return new ReasonixHooks({
|
|
9544
|
+
outputRoot,
|
|
9545
|
+
relativeDirPath: paths.relativeDirPath,
|
|
9546
|
+
relativeFilePath: paths.relativeFilePath,
|
|
9547
|
+
fileContent,
|
|
9548
|
+
validate
|
|
9549
|
+
});
|
|
9550
|
+
}
|
|
9551
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
9552
|
+
const paths = ReasonixHooks.getSettablePaths({ global });
|
|
9553
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
9554
|
+
const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
|
|
9555
|
+
let settings;
|
|
9556
|
+
try {
|
|
9557
|
+
settings = JSON.parse(existingContent);
|
|
9558
|
+
} catch (error) {
|
|
9559
|
+
throw new Error(`Failed to parse existing Reasonix settings at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
9560
|
+
}
|
|
9561
|
+
const config = rulesyncHooks.getJson();
|
|
9562
|
+
const reasonixHooks = canonicalToReasonixHooks({
|
|
9563
|
+
config,
|
|
9564
|
+
toolOverrideHooks: config.reasonix?.hooks,
|
|
9565
|
+
logger
|
|
9566
|
+
});
|
|
9567
|
+
const merged = {
|
|
9568
|
+
...settings,
|
|
9569
|
+
hooks: reasonixHooks
|
|
9570
|
+
};
|
|
9571
|
+
const fileContent = JSON.stringify(merged, null, 2);
|
|
9572
|
+
return new ReasonixHooks({
|
|
9573
|
+
outputRoot,
|
|
9574
|
+
relativeDirPath: paths.relativeDirPath,
|
|
9575
|
+
relativeFilePath: paths.relativeFilePath,
|
|
9576
|
+
fileContent,
|
|
9577
|
+
validate
|
|
9578
|
+
});
|
|
9579
|
+
}
|
|
9580
|
+
toRulesyncHooks() {
|
|
9581
|
+
let settings;
|
|
9582
|
+
try {
|
|
9583
|
+
settings = JSON.parse(this.getFileContent());
|
|
9584
|
+
} catch (error) {
|
|
9585
|
+
throw new Error(`Failed to parse Reasonix hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
9586
|
+
}
|
|
9587
|
+
const hooks = reasonixHooksToCanonical(settings.hooks);
|
|
9588
|
+
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
|
|
9589
|
+
version: 1,
|
|
9590
|
+
hooks
|
|
9591
|
+
}, null, 2) });
|
|
9592
|
+
}
|
|
9593
|
+
validate() {
|
|
9594
|
+
return {
|
|
9595
|
+
success: true,
|
|
9596
|
+
error: null
|
|
9597
|
+
};
|
|
9598
|
+
}
|
|
9599
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
9600
|
+
return new ReasonixHooks({
|
|
9601
|
+
outputRoot,
|
|
9602
|
+
relativeDirPath,
|
|
9603
|
+
relativeFilePath,
|
|
9604
|
+
fileContent: JSON.stringify({ hooks: {} }, null, 2),
|
|
9605
|
+
validate: false
|
|
9606
|
+
});
|
|
9607
|
+
}
|
|
9608
|
+
};
|
|
9609
|
+
//#endregion
|
|
8525
9610
|
//#region src/features/hooks/vibe-hooks.ts
|
|
8526
9611
|
const VIBE_DIR = ".vibe";
|
|
8527
9612
|
const VIBE_HOOKS_FILE_NAME = "hooks.toml";
|
|
@@ -8680,6 +9765,12 @@ var VibeHooks = class VibeHooks extends ToolHooks {
|
|
|
8680
9765
|
relativeFilePath: VIBE_HOOKS_FILE_NAME
|
|
8681
9766
|
};
|
|
8682
9767
|
}
|
|
9768
|
+
static getExtraSharedWritePaths() {
|
|
9769
|
+
return [{
|
|
9770
|
+
relativeDirPath: VIBE_DIR,
|
|
9771
|
+
relativeFilePath: VIBE_CONFIG_FILE_NAME
|
|
9772
|
+
}];
|
|
9773
|
+
}
|
|
8683
9774
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
8684
9775
|
const paths = VibeHooks.getSettablePaths({ global });
|
|
8685
9776
|
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({});
|
|
@@ -8747,6 +9838,19 @@ var VibeHooks = class VibeHooks extends ToolHooks {
|
|
|
8747
9838
|
//#endregion
|
|
8748
9839
|
//#region src/features/hooks/hooks-processor.ts
|
|
8749
9840
|
const HooksProcessorToolTargetSchema = zod_mini.z.enum(hooksProcessorToolTargetTuple);
|
|
9841
|
+
/**
|
|
9842
|
+
* Event names present in the config that the target's adapter cannot emit.
|
|
9843
|
+
*
|
|
9844
|
+
* When the factory passes override-block keys through verbatim
|
|
9845
|
+
* (`passthroughOverrideEvents`), those keys are excluded from the check so
|
|
9846
|
+
* documented passthrough triggers aren't falsely reported as skipped.
|
|
9847
|
+
*/
|
|
9848
|
+
function unsupportedEventNames(params) {
|
|
9849
|
+
const { factory, sharedHooks, effectiveHooks } = params;
|
|
9850
|
+
const supportedEvents = new Set(factory.supportedEvents);
|
|
9851
|
+
const eventNames = factory.passthroughOverrideEvents ? Object.keys(sharedHooks) : Object.keys(effectiveHooks);
|
|
9852
|
+
return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
|
|
9853
|
+
}
|
|
8750
9854
|
const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
8751
9855
|
["antigravity-cli", {
|
|
8752
9856
|
class: AntigravityCliHooks,
|
|
@@ -8880,12 +9984,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
8880
9984
|
supportsGlobal: true,
|
|
8881
9985
|
supportsImport: true
|
|
8882
9986
|
},
|
|
8883
|
-
supportedEvents:
|
|
8884
|
-
supportedHookTypes: [
|
|
8885
|
-
"command",
|
|
8886
|
-
"prompt",
|
|
8887
|
-
"http"
|
|
8888
|
-
],
|
|
9987
|
+
supportedEvents: HERMESAGENT_HOOK_EVENTS,
|
|
9988
|
+
supportedHookTypes: ["command"],
|
|
8889
9989
|
supportsMatcher: true
|
|
8890
9990
|
}],
|
|
8891
9991
|
["deepagents", {
|
|
@@ -8921,6 +10021,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
8921
10021
|
supportedHookTypes: ["command"],
|
|
8922
10022
|
supportsMatcher: true
|
|
8923
10023
|
}],
|
|
10024
|
+
["kiro-ide", {
|
|
10025
|
+
class: KiroIdeHooks,
|
|
10026
|
+
meta: {
|
|
10027
|
+
supportsProject: true,
|
|
10028
|
+
supportsGlobal: true,
|
|
10029
|
+
supportsImport: true
|
|
10030
|
+
},
|
|
10031
|
+
supportedEvents: KIRO_IDE_HOOK_EVENTS,
|
|
10032
|
+
supportedHookTypes: ["command", "prompt"],
|
|
10033
|
+
supportsMatcher: true,
|
|
10034
|
+
passthroughOverrideEvents: true
|
|
10035
|
+
}],
|
|
8924
10036
|
["devin", {
|
|
8925
10037
|
class: DevinHooks,
|
|
8926
10038
|
meta: {
|
|
@@ -8929,8 +10041,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
8929
10041
|
supportsImport: true
|
|
8930
10042
|
},
|
|
8931
10043
|
supportedEvents: DEVIN_HOOK_EVENTS,
|
|
8932
|
-
supportedHookTypes: ["command"],
|
|
8933
|
-
supportsMatcher:
|
|
10044
|
+
supportedHookTypes: ["command", "prompt"],
|
|
10045
|
+
supportsMatcher: true
|
|
8934
10046
|
}],
|
|
8935
10047
|
["augmentcode", {
|
|
8936
10048
|
class: AugmentcodeHooks,
|
|
@@ -8975,6 +10087,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
8975
10087
|
supportedEvents: QWENCODE_HOOK_EVENTS,
|
|
8976
10088
|
supportedHookTypes: ["command"],
|
|
8977
10089
|
supportsMatcher: true
|
|
10090
|
+
}],
|
|
10091
|
+
["reasonix", {
|
|
10092
|
+
class: ReasonixHooks,
|
|
10093
|
+
meta: {
|
|
10094
|
+
supportsProject: true,
|
|
10095
|
+
supportsGlobal: true,
|
|
10096
|
+
supportsImport: true
|
|
10097
|
+
},
|
|
10098
|
+
supportedEvents: REASONIX_HOOK_EVENTS,
|
|
10099
|
+
supportedHookTypes: ["command"],
|
|
10100
|
+
supportsMatcher: true
|
|
8978
10101
|
}]
|
|
8979
10102
|
]);
|
|
8980
10103
|
const hooksProcessorToolTargets = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject).map(([t]) => t);
|
|
@@ -9050,8 +10173,11 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
9050
10173
|
...overrideHooks
|
|
9051
10174
|
};
|
|
9052
10175
|
{
|
|
9053
|
-
const
|
|
9054
|
-
|
|
10176
|
+
const skipped = unsupportedEventNames({
|
|
10177
|
+
factory,
|
|
10178
|
+
sharedHooks,
|
|
10179
|
+
effectiveHooks
|
|
10180
|
+
});
|
|
9055
10181
|
if (skipped.length > 0) this.logger.warn(`Skipped hook event(s) for ${this.toolTarget} (not supported): ${skipped.join(", ")}`);
|
|
9056
10182
|
}
|
|
9057
10183
|
{
|
|
@@ -9247,11 +10373,6 @@ var AiassistantIgnore = class AiassistantIgnore extends ToolIgnore {
|
|
|
9247
10373
|
}
|
|
9248
10374
|
};
|
|
9249
10375
|
//#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
10376
|
//#region src/features/ignore/antigravity-cli-ignore.ts
|
|
9256
10377
|
/**
|
|
9257
10378
|
* Antigravity CLI is the successor to Gemini CLI and is built on the same
|
|
@@ -10483,9 +11604,9 @@ function parseAmpSettingsJsonc(fileContent) {
|
|
|
10483
11604
|
}
|
|
10484
11605
|
function filterMcpServers(mcpServers) {
|
|
10485
11606
|
const filtered = {};
|
|
10486
|
-
if (!isRecord
|
|
11607
|
+
if (!isRecord(mcpServers)) return filtered;
|
|
10487
11608
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
10488
|
-
if (isPrototypePollutionKey(name) || !isRecord
|
|
11609
|
+
if (isPrototypePollutionKey(name) || !isRecord(config)) continue;
|
|
10489
11610
|
const filteredConfig = {};
|
|
10490
11611
|
for (const [key, value] of Object.entries(config)) {
|
|
10491
11612
|
if (isPrototypePollutionKey(key)) continue;
|
|
@@ -10600,7 +11721,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
10600
11721
|
success: true,
|
|
10601
11722
|
error: null
|
|
10602
11723
|
};
|
|
10603
|
-
if (!isRecord
|
|
11724
|
+
if (!isRecord(mcpServers)) return {
|
|
10604
11725
|
success: false,
|
|
10605
11726
|
error: /* @__PURE__ */ new Error(`${AMP_MCP_SERVERS_KEY} must be a JSON object`)
|
|
10606
11727
|
};
|
|
@@ -10609,7 +11730,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
10609
11730
|
success: false,
|
|
10610
11731
|
error: /* @__PURE__ */ new Error(`Server name "${serverName}" is a prototype pollution key and is not allowed`)
|
|
10611
11732
|
};
|
|
10612
|
-
if (!isRecord
|
|
11733
|
+
if (!isRecord(serverConfig)) return {
|
|
10613
11734
|
success: false,
|
|
10614
11735
|
error: /* @__PURE__ */ new Error(`MCP server "${serverName}" must be a JSON object`)
|
|
10615
11736
|
};
|
|
@@ -11130,7 +12251,7 @@ const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
|
|
|
11130
12251
|
function convertFromCodexFormat(codexMcp) {
|
|
11131
12252
|
const result = {};
|
|
11132
12253
|
for (const [name, config] of Object.entries(codexMcp)) {
|
|
11133
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
12254
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
11134
12255
|
const converted = {};
|
|
11135
12256
|
for (const [key, value] of Object.entries(config)) {
|
|
11136
12257
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
@@ -11150,7 +12271,7 @@ function convertToCodexFormat(mcpServers) {
|
|
|
11150
12271
|
const result = {};
|
|
11151
12272
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
11152
12273
|
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
11153
|
-
if (!isRecord
|
|
12274
|
+
if (!isRecord(config)) continue;
|
|
11154
12275
|
const converted = {};
|
|
11155
12276
|
for (const [key, value] of Object.entries(config)) {
|
|
11156
12277
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
@@ -11212,19 +12333,19 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
|
|
|
11212
12333
|
const strippedMcpServers = rulesyncMcp.getMcpServers();
|
|
11213
12334
|
const rawMcpServers = rulesyncMcp.getJson().mcpServers;
|
|
11214
12335
|
const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
|
|
11215
|
-
const rawServer = isRecord
|
|
12336
|
+
const rawServer = isRecord(rawMcpServers) ? rawMcpServers[serverName] : void 0;
|
|
11216
12337
|
return [serverName, {
|
|
11217
12338
|
...serverConfig,
|
|
11218
|
-
...isRecord
|
|
12339
|
+
...isRecord(rawServer) && isStringArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {}
|
|
11219
12340
|
}];
|
|
11220
12341
|
})));
|
|
11221
12342
|
const filteredMcpServers = this.removeEmptyEntries(converted);
|
|
11222
12343
|
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
|
|
12344
|
+
const existingMcpServers = isRecord(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
|
|
11224
12345
|
configToml["mcp_servers"] = Object.fromEntries(Object.entries(filteredMcpServers).map(([name, serverConfig]) => {
|
|
11225
|
-
const existingServer = isRecord
|
|
12346
|
+
const existingServer = isRecord(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
|
|
11226
12347
|
const serverRecord = serverConfig;
|
|
11227
|
-
if (existingServer && isRecord
|
|
12348
|
+
if (existingServer && isRecord(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
|
|
11228
12349
|
...serverRecord,
|
|
11229
12350
|
tools: existingServer["tools"]
|
|
11230
12351
|
}];
|
|
@@ -11734,18 +12855,20 @@ var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
|
|
|
11734
12855
|
//#endregion
|
|
11735
12856
|
//#region src/features/mcp/devin-mcp.ts
|
|
11736
12857
|
/**
|
|
11737
|
-
* MCP generator for Devin (
|
|
12858
|
+
* MCP generator for Devin Local (native `.devin/` configuration).
|
|
12859
|
+
*
|
|
12860
|
+
* Devin reads MCP servers from the `mcpServers` key of its native config file:
|
|
12861
|
+
* - Project scope: `.devin/config.json`
|
|
12862
|
+
* - Global scope: `~/.config/devin/config.json`
|
|
11738
12863
|
*
|
|
11739
|
-
*
|
|
11740
|
-
*
|
|
11741
|
-
*
|
|
12864
|
+
* The config file is shared with the permissions feature (`permissions` key)
|
|
12865
|
+
* and, in global mode, the hooks feature (`hooks` key), so reads and writes
|
|
12866
|
+
* merge into the existing JSON rather than overwriting it, and the file is
|
|
12867
|
+
* never deleted. Each server is a stdio entry ({ command, args, env }) or a
|
|
12868
|
+
* remote entry ({ serverUrl | url, headers }), and may carry an optional
|
|
12869
|
+
* `disabledTools` array.
|
|
11742
12870
|
*
|
|
11743
|
-
*
|
|
11744
|
-
* the same `mcp_config.json` filename so both scopes fit the processor
|
|
11745
|
-
* framework cleanly. The top-level key is `mcpServers`; each server is a
|
|
11746
|
-
* stdio entry ({ command, args, env }) or a remote entry
|
|
11747
|
-
* ({ serverUrl | url, headers }), and may carry an optional `disabledTools`
|
|
11748
|
-
* array.
|
|
12871
|
+
* @see https://docs.devin.ai/cli/extensibility/configuration
|
|
11749
12872
|
*/
|
|
11750
12873
|
var DevinMcp = class DevinMcp extends ToolMcp {
|
|
11751
12874
|
json;
|
|
@@ -11763,14 +12886,21 @@ var DevinMcp = class DevinMcp extends ToolMcp {
|
|
|
11763
12886
|
throw new Error(`Failed to parse Devin MCP config at ${(0, node_path.join)(relativeDirPath, relativeFilePath)}: ${formatError(error)}`, { cause: error });
|
|
11764
12887
|
}
|
|
11765
12888
|
}
|
|
12889
|
+
/**
|
|
12890
|
+
* config.json may carry the permissions/hooks features' keys, so it is never
|
|
12891
|
+
* deleted; only the managed `mcpServers` key is rewritten.
|
|
12892
|
+
*/
|
|
12893
|
+
isDeletable() {
|
|
12894
|
+
return false;
|
|
12895
|
+
}
|
|
11766
12896
|
static getSettablePaths({ global = false } = {}) {
|
|
11767
12897
|
if (global) return {
|
|
11768
|
-
relativeDirPath:
|
|
11769
|
-
relativeFilePath:
|
|
12898
|
+
relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
|
|
12899
|
+
relativeFilePath: DEVIN_CONFIG_FILE_NAME
|
|
11770
12900
|
};
|
|
11771
12901
|
return {
|
|
11772
|
-
relativeDirPath:
|
|
11773
|
-
relativeFilePath:
|
|
12902
|
+
relativeDirPath: DEVIN_DIR,
|
|
12903
|
+
relativeFilePath: DEVIN_CONFIG_FILE_NAME
|
|
11774
12904
|
};
|
|
11775
12905
|
}
|
|
11776
12906
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
@@ -11885,7 +13015,7 @@ var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
|
|
|
11885
13015
|
};
|
|
11886
13016
|
//#endregion
|
|
11887
13017
|
//#region src/features/mcp/goose-mcp.ts
|
|
11888
|
-
const
|
|
13018
|
+
const GOOSE_PLUGIN_MCP_RELATIVE_PATH = (0, node_path.join)(GOOSE_PLUGIN_MCP_DIR_PATH, GOOSE_PLUGIN_MCP_FILE_NAME);
|
|
11889
13019
|
function parseGooseConfig(fileContent, relativeDirPath, relativeFilePath) {
|
|
11890
13020
|
const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
|
|
11891
13021
|
let parsed;
|
|
@@ -11972,7 +13102,7 @@ function convertServerToGooseExtension(name, config) {
|
|
|
11972
13102
|
function convertToGooseFormat(mcpServers) {
|
|
11973
13103
|
const extensions = {};
|
|
11974
13104
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
11975
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
13105
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
11976
13106
|
extensions[name] = convertServerToGooseExtension(name, config);
|
|
11977
13107
|
}
|
|
11978
13108
|
return extensions;
|
|
@@ -11989,7 +13119,7 @@ function convertToGooseFormat(mcpServers) {
|
|
|
11989
13119
|
function convertFromGooseFormat(extensions) {
|
|
11990
13120
|
const result = {};
|
|
11991
13121
|
for (const [name, ext] of Object.entries(extensions)) {
|
|
11992
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
13122
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(ext)) continue;
|
|
11993
13123
|
const server = {};
|
|
11994
13124
|
const type = typeof ext.type === "string" ? ext.type : void 0;
|
|
11995
13125
|
if (type === "sse") server.type = "sse";
|
|
@@ -12007,39 +13137,102 @@ function convertFromGooseFormat(extensions) {
|
|
|
12007
13137
|
return result;
|
|
12008
13138
|
}
|
|
12009
13139
|
/**
|
|
13140
|
+
* Builds a Claude-style stdio server entry (`command`/`args`/`env`/`cwd`) for the
|
|
13141
|
+
* open-plugins `.mcp.json` manifest.
|
|
13142
|
+
*/
|
|
13143
|
+
function buildGoosePluginStdioServer(config) {
|
|
13144
|
+
const server = {};
|
|
13145
|
+
const command = config.command;
|
|
13146
|
+
if (Array.isArray(command)) {
|
|
13147
|
+
if (typeof command[0] === "string") server.command = command[0];
|
|
13148
|
+
const rest = command.slice(1).filter((c) => typeof c === "string");
|
|
13149
|
+
const args = isStringArray(config.args) ? config.args : [];
|
|
13150
|
+
if (rest.length > 0 || args.length > 0) server.args = [...rest, ...args];
|
|
13151
|
+
} else if (typeof command === "string") {
|
|
13152
|
+
server.command = command;
|
|
13153
|
+
if (isStringArray(config.args)) server.args = config.args;
|
|
13154
|
+
}
|
|
13155
|
+
if (isPlainObject(config.env)) server.env = omitPrototypePollutionKeys(config.env);
|
|
13156
|
+
if (typeof config.cwd === "string") server.cwd = config.cwd;
|
|
13157
|
+
return server;
|
|
13158
|
+
}
|
|
13159
|
+
/**
|
|
13160
|
+
* Converts rulesync canonical MCP servers into the Claude-style `mcpServers` map
|
|
13161
|
+
* used by Goose open-plugin manifests (`.agents/plugins/rulesync/.mcp.json`).
|
|
13162
|
+
*
|
|
13163
|
+
* The open-plugins manifest is **stdio-only** (no `url`/`headers`), so remote
|
|
13164
|
+
* (http/sse/streamable_http) and `builtin` servers cannot be represented. They
|
|
13165
|
+
* are skipped with a warning rather than silently dropped — remote servers stay
|
|
13166
|
+
* global-config-only (sync them with `--global` to `~/.config/goose/config.yaml`).
|
|
13167
|
+
*/
|
|
13168
|
+
function convertToGoosePluginMcpServers(mcpServers, logger) {
|
|
13169
|
+
const result = {};
|
|
13170
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
13171
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
13172
|
+
const gooseType = resolveGooseType(config, resolveGooseUrl(config));
|
|
13173
|
+
if (gooseType !== "stdio") {
|
|
13174
|
+
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.`);
|
|
13175
|
+
continue;
|
|
13176
|
+
}
|
|
13177
|
+
result[name] = buildGoosePluginStdioServer(config);
|
|
13178
|
+
}
|
|
13179
|
+
return result;
|
|
13180
|
+
}
|
|
13181
|
+
/**
|
|
12010
13182
|
* Goose MCP servers.
|
|
12011
13183
|
*
|
|
12012
|
-
* Goose configures MCP servers
|
|
12013
|
-
*
|
|
12014
|
-
*
|
|
12015
|
-
*
|
|
12016
|
-
*
|
|
13184
|
+
* Goose configures MCP servers in two locations:
|
|
13185
|
+
*
|
|
13186
|
+
* - **Global** (`--global`): "extensions" in the shared user config
|
|
13187
|
+
* `~/.config/goose/config.yaml`. That file also holds other Goose settings
|
|
13188
|
+
* (model, provider, ...), so generation merges the `extensions:` block into the
|
|
13189
|
+
* existing config instead of overwriting it, and the file is never deleted.
|
|
13190
|
+
* This location supports both stdio and remote (http/sse) servers.
|
|
13191
|
+
* - **Project**: a stdio-only open-plugin manifest at
|
|
13192
|
+
* `.agents/plugins/rulesync/.mcp.json` (Goose v1.39.0+), reusing the same
|
|
13193
|
+
* `.agents/plugins/rulesync/` tree as Goose hooks. The manifest uses the
|
|
13194
|
+
* Claude-style `{ "mcpServers": { "<name>": { command, args, env, cwd } } }`
|
|
13195
|
+
* shape and cannot express `url`/`headers`, so remote servers are skipped with
|
|
13196
|
+
* a warning in project mode (use `--global` to sync them instead).
|
|
12017
13197
|
*
|
|
12018
13198
|
* @see https://block.github.io/goose/docs/getting-started/using-extensions/
|
|
13199
|
+
* @see https://github.com/block/goose/pull/9471
|
|
12019
13200
|
*/
|
|
12020
13201
|
var GooseMcp = class GooseMcp extends ToolMcp {
|
|
12021
13202
|
config;
|
|
12022
13203
|
constructor(params) {
|
|
12023
13204
|
super(params);
|
|
12024
|
-
if (this.fileContent
|
|
12025
|
-
else this.config =
|
|
13205
|
+
if (this.fileContent === void 0) this.config = {};
|
|
13206
|
+
else if (params.global) this.config = parseGooseConfig(this.fileContent, this.relativeDirPath, this.relativeFilePath);
|
|
13207
|
+
else this.config = this.fileContent ? this.parsePluginManifest(this.fileContent) : {};
|
|
13208
|
+
}
|
|
13209
|
+
parsePluginManifest(fileContent) {
|
|
13210
|
+
try {
|
|
13211
|
+
const parsed = JSON.parse(fileContent);
|
|
13212
|
+
return isRecord(parsed) ? parsed : {};
|
|
13213
|
+
} catch (error) {
|
|
13214
|
+
throw new Error(`Failed to parse Goose MCP manifest at ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(error)}`, { cause: error });
|
|
13215
|
+
}
|
|
12026
13216
|
}
|
|
12027
13217
|
getConfig() {
|
|
12028
13218
|
return this.config;
|
|
12029
13219
|
}
|
|
12030
13220
|
isDeletable() {
|
|
12031
|
-
return
|
|
13221
|
+
return !this.global;
|
|
12032
13222
|
}
|
|
12033
|
-
static getSettablePaths(
|
|
12034
|
-
return {
|
|
13223
|
+
static getSettablePaths({ global = false } = {}) {
|
|
13224
|
+
if (global) return {
|
|
12035
13225
|
relativeDirPath: GOOSE_GLOBAL_DIR,
|
|
12036
13226
|
relativeFilePath: GOOSE_MCP_FILE_NAME
|
|
12037
13227
|
};
|
|
13228
|
+
return {
|
|
13229
|
+
relativeDirPath: GOOSE_PLUGIN_MCP_DIR_PATH,
|
|
13230
|
+
relativeFilePath: GOOSE_PLUGIN_MCP_FILE_NAME
|
|
13231
|
+
};
|
|
12038
13232
|
}
|
|
12039
13233
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
12040
|
-
if (!global) throw new Error(GOOSE_GLOBAL_ONLY_MESSAGE$1);
|
|
12041
13234
|
const paths = this.getSettablePaths({ global });
|
|
12042
|
-
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
13235
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? (global ? "" : JSON.stringify({ mcpServers: {} }, null, 2));
|
|
12043
13236
|
return new GooseMcp({
|
|
12044
13237
|
outputRoot,
|
|
12045
13238
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -12049,9 +13242,19 @@ var GooseMcp = class GooseMcp extends ToolMcp {
|
|
|
12049
13242
|
global
|
|
12050
13243
|
});
|
|
12051
13244
|
}
|
|
12052
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
12053
|
-
if (!global) throw new Error(GOOSE_GLOBAL_ONLY_MESSAGE$1);
|
|
13245
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
12054
13246
|
const paths = this.getSettablePaths({ global });
|
|
13247
|
+
if (!global) {
|
|
13248
|
+
const mcpServers = convertToGoosePluginMcpServers(rulesyncMcp.getMcpServers(), logger);
|
|
13249
|
+
return new GooseMcp({
|
|
13250
|
+
outputRoot,
|
|
13251
|
+
relativeDirPath: paths.relativeDirPath,
|
|
13252
|
+
relativeFilePath: paths.relativeFilePath,
|
|
13253
|
+
fileContent: JSON.stringify({ mcpServers }, null, 2),
|
|
13254
|
+
validate,
|
|
13255
|
+
global
|
|
13256
|
+
});
|
|
13257
|
+
}
|
|
12055
13258
|
const merged = {
|
|
12056
13259
|
...parseGooseConfig(await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), ""), paths.relativeDirPath, paths.relativeFilePath),
|
|
12057
13260
|
extensions: convertToGooseFormat(rulesyncMcp.getMcpServers())
|
|
@@ -12066,7 +13269,11 @@ var GooseMcp = class GooseMcp extends ToolMcp {
|
|
|
12066
13269
|
});
|
|
12067
13270
|
}
|
|
12068
13271
|
toRulesyncMcp() {
|
|
12069
|
-
|
|
13272
|
+
if (!this.global) {
|
|
13273
|
+
const mcpServers = isRecord(this.config.mcpServers) ? this.config.mcpServers : {};
|
|
13274
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
13275
|
+
}
|
|
13276
|
+
const mcpServers = convertFromGooseFormat(isRecord(this.config.extensions) ? this.config.extensions : {});
|
|
12070
13277
|
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
12071
13278
|
}
|
|
12072
13279
|
validate() {
|
|
@@ -12143,7 +13350,7 @@ function convertToGrokFormat(mcpServers) {
|
|
|
12143
13350
|
const result = {};
|
|
12144
13351
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
12145
13352
|
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
12146
|
-
if (!isRecord
|
|
13353
|
+
if (!isRecord(config)) continue;
|
|
12147
13354
|
const converted = {};
|
|
12148
13355
|
for (const [key, value] of Object.entries(config)) {
|
|
12149
13356
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
@@ -12158,7 +13365,7 @@ function convertToGrokFormat(mcpServers) {
|
|
|
12158
13365
|
function convertFromGrokFormat(grokMcp) {
|
|
12159
13366
|
const result = {};
|
|
12160
13367
|
for (const [name, config] of Object.entries(grokMcp)) {
|
|
12161
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
13368
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
12162
13369
|
const converted = {};
|
|
12163
13370
|
for (const [key, value] of Object.entries(config)) {
|
|
12164
13371
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
@@ -12321,13 +13528,13 @@ function convertServerToHermes(config) {
|
|
|
12321
13528
|
function convertToHermesFormat(mcpServers) {
|
|
12322
13529
|
const result = {};
|
|
12323
13530
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
12324
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
13531
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
12325
13532
|
result[name] = convertServerToHermes(config);
|
|
12326
13533
|
}
|
|
12327
13534
|
return result;
|
|
12328
13535
|
}
|
|
12329
13536
|
function mergeHermesMcpServers(config, mcpServers) {
|
|
12330
|
-
const existingMcpServers = isRecord
|
|
13537
|
+
const existingMcpServers = isRecord(config.mcp_servers) ? config.mcp_servers : {};
|
|
12331
13538
|
return {
|
|
12332
13539
|
...config,
|
|
12333
13540
|
mcp_servers: {
|
|
@@ -12345,7 +13552,7 @@ function mergeHermesMcpServers(config, mcpServers) {
|
|
|
12345
13552
|
function convertFromHermesFormat(mcpServers) {
|
|
12346
13553
|
const result = {};
|
|
12347
13554
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
12348
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord
|
|
13555
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
|
|
12349
13556
|
const server = {};
|
|
12350
13557
|
if (typeof config.command === "string") server.command = config.command;
|
|
12351
13558
|
if (isStringArray(config.args)) server.args = config.args;
|
|
@@ -12381,7 +13588,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
12381
13588
|
return true;
|
|
12382
13589
|
}
|
|
12383
13590
|
setFileContent(fileContent) {
|
|
12384
|
-
const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord
|
|
13591
|
+
const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
12385
13592
|
this.config = merged;
|
|
12386
13593
|
super.setFileContent(stringifyHermesConfig(merged));
|
|
12387
13594
|
}
|
|
@@ -12421,7 +13628,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
12421
13628
|
});
|
|
12422
13629
|
}
|
|
12423
13630
|
toRulesyncMcp() {
|
|
12424
|
-
const servers = convertFromHermesFormat(isRecord
|
|
13631
|
+
const servers = convertFromHermesFormat(isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
12425
13632
|
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: servers }, null, 2) });
|
|
12426
13633
|
}
|
|
12427
13634
|
validate() {
|
|
@@ -13256,11 +14463,6 @@ var QwencodeMcp = class QwencodeMcp extends ToolMcp {
|
|
|
13256
14463
|
}
|
|
13257
14464
|
};
|
|
13258
14465
|
//#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
14466
|
//#region src/features/mcp/reasonix-mcp.ts
|
|
13265
14467
|
const REASONIX_PLUGIN_FIELDS = [
|
|
13266
14468
|
"type",
|
|
@@ -13268,13 +14470,14 @@ const REASONIX_PLUGIN_FIELDS = [
|
|
|
13268
14470
|
"args",
|
|
13269
14471
|
"env",
|
|
13270
14472
|
"url",
|
|
13271
|
-
"headers"
|
|
14473
|
+
"headers",
|
|
14474
|
+
"trusted_read_only_tools"
|
|
13272
14475
|
];
|
|
13273
14476
|
var ReasonixMcp = class ReasonixMcp extends ToolMcp {
|
|
13274
14477
|
toml;
|
|
13275
14478
|
constructor(params) {
|
|
13276
14479
|
super(params);
|
|
13277
|
-
this.toml = parseReasonixConfig(this.fileContent);
|
|
14480
|
+
this.toml = parseReasonixConfig$1(this.fileContent);
|
|
13278
14481
|
}
|
|
13279
14482
|
getToml() {
|
|
13280
14483
|
return this.toml;
|
|
@@ -13298,7 +14501,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
|
|
|
13298
14501
|
}
|
|
13299
14502
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
13300
14503
|
const paths = this.getSettablePaths({ global });
|
|
13301
|
-
const config = parseReasonixConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
|
|
14504
|
+
const config = parseReasonixConfig$1(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
|
|
13302
14505
|
config.plugins = normalizePluginsArray(config.plugins);
|
|
13303
14506
|
return new ReasonixMcp({
|
|
13304
14507
|
outputRoot,
|
|
@@ -13311,7 +14514,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
|
|
|
13311
14514
|
}
|
|
13312
14515
|
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
13313
14516
|
const paths = this.getSettablePaths({ global });
|
|
13314
|
-
const config = parseReasonixConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
|
|
14517
|
+
const config = parseReasonixConfig$1(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
|
|
13315
14518
|
config.plugins = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToReasonix(name, server));
|
|
13316
14519
|
return new ReasonixMcp({
|
|
13317
14520
|
outputRoot,
|
|
@@ -13328,7 +14531,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
|
|
|
13328
14531
|
}
|
|
13329
14532
|
validate() {
|
|
13330
14533
|
try {
|
|
13331
|
-
parseReasonixConfig(this.fileContent);
|
|
14534
|
+
parseReasonixConfig$1(this.fileContent);
|
|
13332
14535
|
return {
|
|
13333
14536
|
success: true,
|
|
13334
14537
|
error: null
|
|
@@ -13351,7 +14554,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
|
|
|
13351
14554
|
});
|
|
13352
14555
|
}
|
|
13353
14556
|
};
|
|
13354
|
-
function parseReasonixConfig(fileContent) {
|
|
14557
|
+
function parseReasonixConfig$1(fileContent) {
|
|
13355
14558
|
const parsed = smol_toml.parse(fileContent || smol_toml.stringify({}));
|
|
13356
14559
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
13357
14560
|
return { ...parsed };
|
|
@@ -13491,17 +14694,6 @@ var RooMcp = class RooMcp extends ToolMcp {
|
|
|
13491
14694
|
}
|
|
13492
14695
|
};
|
|
13493
14696
|
//#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
14697
|
//#region src/features/mcp/rovodev-mcp.ts
|
|
13506
14698
|
function parseRovodevMcpJson(fileContent, relativeDirPath, relativeFilePath) {
|
|
13507
14699
|
const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
|
|
@@ -13595,6 +14787,178 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
13595
14787
|
}
|
|
13596
14788
|
};
|
|
13597
14789
|
//#endregion
|
|
14790
|
+
//#region src/features/shared/takt-config.ts
|
|
14791
|
+
/**
|
|
14792
|
+
* Parse a Takt `config.yaml` into a plain object, treating an empty file as `{}`.
|
|
14793
|
+
*
|
|
14794
|
+
* Shared by the Takt adapters that read-modify-write the same `config.yaml`
|
|
14795
|
+
* (mcp, permissions, ...). Uses `isPlainObject` (not `isRecord`) so class
|
|
14796
|
+
* instances are rejected for prototype-pollution hardening; a YAML mapping
|
|
14797
|
+
* always parses to a plain object.
|
|
14798
|
+
*/
|
|
14799
|
+
function parseTaktConfig(fileContent, relativeDirPath, relativeFilePath) {
|
|
14800
|
+
const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
|
|
14801
|
+
let parsed;
|
|
14802
|
+
try {
|
|
14803
|
+
parsed = fileContent.trim() === "" ? {} : (0, js_yaml.load)(fileContent);
|
|
14804
|
+
} catch (error) {
|
|
14805
|
+
throw new Error(`Failed to parse Takt config at ${configPath}: ${formatError(error)}`, { cause: error });
|
|
14806
|
+
}
|
|
14807
|
+
if (parsed === void 0 || parsed === null) return {};
|
|
14808
|
+
if (!isPlainObject(parsed)) throw new Error(`Failed to parse Takt config at ${configPath}: expected a YAML mapping`);
|
|
14809
|
+
return parsed;
|
|
14810
|
+
}
|
|
14811
|
+
//#endregion
|
|
14812
|
+
//#region src/features/mcp/takt-mcp.ts
|
|
14813
|
+
/**
|
|
14814
|
+
* MCP adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
|
|
14815
|
+
* global).
|
|
14816
|
+
*
|
|
14817
|
+
* IMPORTANT — what this adapter can and cannot represent.
|
|
14818
|
+
*
|
|
14819
|
+
* Takt does NOT have a project- or global-level registry of MCP *server
|
|
14820
|
+
* definitions*. The concrete `mcp_servers` map (a server's `command`/`args`/`env`
|
|
14821
|
+
* or `type`/`url`/`headers`) is declared per-step inside individual *workflow*
|
|
14822
|
+
* YAML files; there is no top-level `mcp_servers` key in `config.yaml`, and the
|
|
14823
|
+
* config loader hard-rejects unknown top-level keys
|
|
14824
|
+
* (`assertNoUnknownGlobalConfigKeys`). Writing a server map into `config.yaml`
|
|
14825
|
+
* would therefore both be ignored and break the user's config.
|
|
14826
|
+
*
|
|
14827
|
+
* What `config.yaml` *does* hold is the default-deny transport allowlist
|
|
14828
|
+
* `workflow_mcp_servers: { stdio, sse, http }`. Without it, workflow-defined MCP
|
|
14829
|
+
* servers are refused regardless of how they are declared. So this adapter emits
|
|
14830
|
+
* the transport allowlist derived from the transports present in
|
|
14831
|
+
* `.rulesync/mcp.json`, enabling exactly the transports the user's servers need.
|
|
14832
|
+
*
|
|
14833
|
+
* Lossiness (documented, intentional): the per-server names, commands, env, URLs
|
|
14834
|
+
* and headers are NOT representable in `config.yaml` and are intentionally not
|
|
14835
|
+
* written. Users still declare the concrete servers in their workflow YAML
|
|
14836
|
+
* steps; rulesync only opens the transport gate that permits them. As a
|
|
14837
|
+
* corollary, import (`toRulesyncMcp`) cannot reconstruct server definitions from
|
|
14838
|
+
* a transport allowlist and yields an empty `mcpServers` map.
|
|
14839
|
+
*
|
|
14840
|
+
* The shared `config.yaml` is merged in place: only the
|
|
14841
|
+
* `workflow_mcp_servers` key is set; every other top-level key (provider,
|
|
14842
|
+
* provider_profiles, etc.) is preserved. The file is never deleted.
|
|
14843
|
+
*
|
|
14844
|
+
* @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
|
|
14845
|
+
* @see https://github.com/nrslib/takt/blob/main/src/core/models/mcp-schemas.ts
|
|
14846
|
+
*/
|
|
14847
|
+
var TaktMcp = class TaktMcp extends ToolMcp {
|
|
14848
|
+
constructor(params) {
|
|
14849
|
+
super({
|
|
14850
|
+
...params,
|
|
14851
|
+
fileContent: params.fileContent ?? ""
|
|
14852
|
+
});
|
|
14853
|
+
}
|
|
14854
|
+
isDeletable() {
|
|
14855
|
+
return false;
|
|
14856
|
+
}
|
|
14857
|
+
static getSettablePaths(_options) {
|
|
14858
|
+
return {
|
|
14859
|
+
relativeDirPath: TAKT_DIR,
|
|
14860
|
+
relativeFilePath: TAKT_CONFIG_FILE_NAME
|
|
14861
|
+
};
|
|
14862
|
+
}
|
|
14863
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
14864
|
+
const paths = TaktMcp.getSettablePaths({ global });
|
|
14865
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
14866
|
+
return new TaktMcp({
|
|
14867
|
+
outputRoot,
|
|
14868
|
+
relativeDirPath: paths.relativeDirPath,
|
|
14869
|
+
relativeFilePath: paths.relativeFilePath,
|
|
14870
|
+
fileContent,
|
|
14871
|
+
validate,
|
|
14872
|
+
global
|
|
14873
|
+
});
|
|
14874
|
+
}
|
|
14875
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
14876
|
+
const paths = TaktMcp.getSettablePaths({ global });
|
|
14877
|
+
const config = parseTaktConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
|
|
14878
|
+
const allowlist = deriveTransportAllowlist(rulesyncMcp.getMcpServers());
|
|
14879
|
+
const merged = {
|
|
14880
|
+
...config,
|
|
14881
|
+
[TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist
|
|
14882
|
+
};
|
|
14883
|
+
return new TaktMcp({
|
|
14884
|
+
outputRoot,
|
|
14885
|
+
relativeDirPath: paths.relativeDirPath,
|
|
14886
|
+
relativeFilePath: paths.relativeFilePath,
|
|
14887
|
+
fileContent: (0, js_yaml.dump)(merged),
|
|
14888
|
+
validate,
|
|
14889
|
+
global
|
|
14890
|
+
});
|
|
14891
|
+
}
|
|
14892
|
+
/**
|
|
14893
|
+
* A transport allowlist cannot reconstruct the per-step server definitions,
|
|
14894
|
+
* so import yields an empty `mcpServers` map. This keeps the round-trip honest
|
|
14895
|
+
* rather than fabricating placeholder servers.
|
|
14896
|
+
*/
|
|
14897
|
+
toRulesyncMcp() {
|
|
14898
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: {} }, null, 2) });
|
|
14899
|
+
}
|
|
14900
|
+
validate() {
|
|
14901
|
+
return {
|
|
14902
|
+
success: true,
|
|
14903
|
+
error: null
|
|
14904
|
+
};
|
|
14905
|
+
}
|
|
14906
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
14907
|
+
return new TaktMcp({
|
|
14908
|
+
outputRoot,
|
|
14909
|
+
relativeDirPath,
|
|
14910
|
+
relativeFilePath,
|
|
14911
|
+
fileContent: "",
|
|
14912
|
+
validate: false,
|
|
14913
|
+
global
|
|
14914
|
+
});
|
|
14915
|
+
}
|
|
14916
|
+
};
|
|
14917
|
+
/**
|
|
14918
|
+
* Map a rulesync MCP server onto the single Takt transport its allowlist gates.
|
|
14919
|
+
*
|
|
14920
|
+
* Takt allows only `stdio` / `sse` / `http`, so the broader rulesync alias set is
|
|
14921
|
+
* folded: `local` ⇒ stdio; `streamable-http` / `ws` ⇒ http. A server with no
|
|
14922
|
+
* explicit transport is treated as stdio when it carries a `command`, else as a
|
|
14923
|
+
* remote `http` server (it must have a `url`). Returns `undefined` only when the
|
|
14924
|
+
* shape is too ambiguous to classify.
|
|
14925
|
+
*/
|
|
14926
|
+
function transportOf(server) {
|
|
14927
|
+
switch (server.type ?? server.transport) {
|
|
14928
|
+
case "stdio":
|
|
14929
|
+
case "local": return "stdio";
|
|
14930
|
+
case "sse": return "sse";
|
|
14931
|
+
case "http":
|
|
14932
|
+
case "streamable-http":
|
|
14933
|
+
case "ws": return "http";
|
|
14934
|
+
default: break;
|
|
14935
|
+
}
|
|
14936
|
+
if (server.command !== void 0) return "stdio";
|
|
14937
|
+
if (server.url !== void 0 || server.httpUrl !== void 0) return "http";
|
|
14938
|
+
}
|
|
14939
|
+
/**
|
|
14940
|
+
* Derive Takt's `workflow_mcp_servers` allowlist from the transports present in
|
|
14941
|
+
* the rulesync servers. All three keys are emitted explicitly (default-deny made
|
|
14942
|
+
* visible): a transport is `true` only when at least one server uses it.
|
|
14943
|
+
*
|
|
14944
|
+
* Server entries are read defensively (record guard); prototype-pollution server
|
|
14945
|
+
* names are irrelevant here because no user-controlled key or value is written —
|
|
14946
|
+
* only the three fixed boolean keys are.
|
|
14947
|
+
*/
|
|
14948
|
+
function deriveTransportAllowlist(servers) {
|
|
14949
|
+
const allowlist = {
|
|
14950
|
+
stdio: false,
|
|
14951
|
+
sse: false,
|
|
14952
|
+
http: false
|
|
14953
|
+
};
|
|
14954
|
+
for (const server of Object.values(servers)) {
|
|
14955
|
+
if (!isRecord(server)) continue;
|
|
14956
|
+
const transport = transportOf(server);
|
|
14957
|
+
if (transport) allowlist[transport] = true;
|
|
14958
|
+
}
|
|
14959
|
+
return allowlist;
|
|
14960
|
+
}
|
|
14961
|
+
//#endregion
|
|
13598
14962
|
//#region src/features/mcp/vibe-mcp.ts
|
|
13599
14963
|
const VIBE_MCP_SERVER_FIELDS = [
|
|
13600
14964
|
"transport",
|
|
@@ -14055,7 +15419,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
14055
15419
|
["goose", {
|
|
14056
15420
|
class: GooseMcp,
|
|
14057
15421
|
meta: {
|
|
14058
|
-
supportsProject:
|
|
15422
|
+
supportsProject: true,
|
|
14059
15423
|
supportsGlobal: true,
|
|
14060
15424
|
supportsEnabledTools: false,
|
|
14061
15425
|
supportsDisabledTools: false
|
|
@@ -14169,6 +15533,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
14169
15533
|
supportsDisabledTools: false
|
|
14170
15534
|
}
|
|
14171
15535
|
}],
|
|
15536
|
+
["takt", {
|
|
15537
|
+
class: TaktMcp,
|
|
15538
|
+
meta: {
|
|
15539
|
+
supportsProject: true,
|
|
15540
|
+
supportsGlobal: true,
|
|
15541
|
+
supportsEnabledTools: false,
|
|
15542
|
+
supportsDisabledTools: false
|
|
15543
|
+
}
|
|
15544
|
+
}],
|
|
14172
15545
|
["vibe", {
|
|
14173
15546
|
class: VibeMcp,
|
|
14174
15547
|
meta: {
|
|
@@ -14297,7 +15670,8 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
14297
15670
|
return await factory.class.fromRulesyncMcp({
|
|
14298
15671
|
outputRoot: this.outputRoot,
|
|
14299
15672
|
rulesyncMcp: filteredRulesyncMcp,
|
|
14300
|
-
global: this.global
|
|
15673
|
+
global: this.global,
|
|
15674
|
+
logger: this.logger
|
|
14301
15675
|
});
|
|
14302
15676
|
}));
|
|
14303
15677
|
}
|
|
@@ -14811,7 +16185,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
|
|
|
14811
16185
|
function toAntigravityCliToolName(canonical) {
|
|
14812
16186
|
return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
|
|
14813
16187
|
}
|
|
14814
|
-
function toCanonicalToolName$
|
|
16188
|
+
function toCanonicalToolName$6(cliName) {
|
|
14815
16189
|
return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
|
|
14816
16190
|
}
|
|
14817
16191
|
/**
|
|
@@ -14994,7 +16368,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
|
|
|
14994
16368
|
const processEntries = (entries, action) => {
|
|
14995
16369
|
for (const entry of entries) {
|
|
14996
16370
|
const { toolName, pattern } = parsePermissionEntry$1(entry);
|
|
14997
|
-
const canonical = toCanonicalToolName$
|
|
16371
|
+
const canonical = toCanonicalToolName$6(toolName);
|
|
14998
16372
|
if (!permission[canonical]) permission[canonical] = {};
|
|
14999
16373
|
permission[canonical][pattern] = action;
|
|
15000
16374
|
}
|
|
@@ -15033,7 +16407,7 @@ const IDE_ACTION_TO_CANONICAL = {
|
|
|
15033
16407
|
function toIdeAction(canonical) {
|
|
15034
16408
|
return CANONICAL_TO_IDE_ACTION[canonical] ?? canonical;
|
|
15035
16409
|
}
|
|
15036
|
-
function toCanonicalCategory$
|
|
16410
|
+
function toCanonicalCategory$2(ideAction) {
|
|
15037
16411
|
return IDE_ACTION_TO_CANONICAL[ideAction] ?? ideAction;
|
|
15038
16412
|
}
|
|
15039
16413
|
/**
|
|
@@ -15207,7 +16581,7 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
|
|
|
15207
16581
|
const processEntries = (entries, action) => {
|
|
15208
16582
|
for (const entry of entries) {
|
|
15209
16583
|
const { action: ideAction, pattern } = parsePermissionEntry(entry);
|
|
15210
|
-
const canonical = toCanonicalCategory$
|
|
16584
|
+
const canonical = toCanonicalCategory$2(ideAction);
|
|
15211
16585
|
if (!permission[canonical]) permission[canonical] = {};
|
|
15212
16586
|
permission[canonical][pattern] = action;
|
|
15213
16587
|
}
|
|
@@ -15256,7 +16630,7 @@ const AUGMENT_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONI
|
|
|
15256
16630
|
function toAugmentToolName(canonical) {
|
|
15257
16631
|
return CANONICAL_TO_AUGMENT_TOOL_NAMES[canonical] ?? canonical;
|
|
15258
16632
|
}
|
|
15259
|
-
function toCanonicalToolName$
|
|
16633
|
+
function toCanonicalToolName$5(augmentName) {
|
|
15260
16634
|
return AUGMENT_TO_CANONICAL_TOOL_NAMES[augmentName] ?? augmentName;
|
|
15261
16635
|
}
|
|
15262
16636
|
function actionToAugmentType(action) {
|
|
@@ -15600,7 +16974,7 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
15600
16974
|
}
|
|
15601
16975
|
const type = entry.permission.type;
|
|
15602
16976
|
if (!isBasicAugmentType(type)) continue;
|
|
15603
|
-
const canonical = toCanonicalToolName$
|
|
16977
|
+
const canonical = toCanonicalToolName$5(entry.toolName);
|
|
15604
16978
|
if (forbiddenMapKeys.has(canonical)) {
|
|
15605
16979
|
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
16980
|
continue;
|
|
@@ -15653,7 +17027,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
|
|
|
15653
17027
|
function toClaudeToolName(canonical) {
|
|
15654
17028
|
return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
|
|
15655
17029
|
}
|
|
15656
|
-
function toCanonicalToolName$
|
|
17030
|
+
function toCanonicalToolName$4(claudeName) {
|
|
15657
17031
|
return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
|
|
15658
17032
|
}
|
|
15659
17033
|
/**
|
|
@@ -15827,7 +17201,7 @@ function convertClaudeToRulesyncPermissions(params) {
|
|
|
15827
17201
|
const processEntries = (entries, action) => {
|
|
15828
17202
|
for (const entry of entries) {
|
|
15829
17203
|
const { toolName, pattern } = parseClaudePermissionEntry(entry);
|
|
15830
|
-
const canonical = toCanonicalToolName$
|
|
17204
|
+
const canonical = toCanonicalToolName$4(toolName);
|
|
15831
17205
|
if (!permission[canonical]) permission[canonical] = {};
|
|
15832
17206
|
permission[canonical][pattern] = action;
|
|
15833
17207
|
}
|
|
@@ -16454,7 +17828,7 @@ function toCursorPattern(canonical, pattern) {
|
|
|
16454
17828
|
}
|
|
16455
17829
|
return pattern;
|
|
16456
17830
|
}
|
|
16457
|
-
function toCanonicalCategory(cursorType, pattern) {
|
|
17831
|
+
function toCanonicalCategory$1(cursorType, pattern) {
|
|
16458
17832
|
if (cursorType === "Mcp") {
|
|
16459
17833
|
const match = pattern.match(/^([^:()]+):([^:()]+)$/);
|
|
16460
17834
|
if (match) return `${MCP_CANONICAL_PREFIX}${match[1] ?? "*"}__${match[2] ?? "*"}`;
|
|
@@ -16679,7 +18053,7 @@ function convertCursorToRulesyncPermissions(params) {
|
|
|
16679
18053
|
const processEntries = (entries, action) => {
|
|
16680
18054
|
for (const entry of entries) {
|
|
16681
18055
|
const { type, pattern } = parseCursorPermissionEntry(entry);
|
|
16682
|
-
const canonical = toCanonicalCategory(type, pattern);
|
|
18056
|
+
const canonical = toCanonicalCategory$1(type, pattern);
|
|
16683
18057
|
if (!permission[canonical]) permission[canonical] = {};
|
|
16684
18058
|
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX) ? "*" : pattern;
|
|
16685
18059
|
permission[canonical][canonicalPattern] = action;
|
|
@@ -16690,6 +18064,245 @@ function convertCursorToRulesyncPermissions(params) {
|
|
|
16690
18064
|
return { permission };
|
|
16691
18065
|
}
|
|
16692
18066
|
//#endregion
|
|
18067
|
+
//#region src/features/permissions/devin-permissions.ts
|
|
18068
|
+
/**
|
|
18069
|
+
* Mapping from rulesync canonical tool category names to Devin Local permission
|
|
18070
|
+
* scope matchers.
|
|
18071
|
+
*
|
|
18072
|
+
* Devin expresses permissions with scope-based matchers — `Read(glob)`,
|
|
18073
|
+
* `Write(glob)`, `Exec(prefix)`, and `Fetch(pattern)` — plus MCP tool patterns
|
|
18074
|
+
* (`mcp__server__tool`). The canonical `edit` and `write` categories both map
|
|
18075
|
+
* onto Devin's single `Write` scope; on import `Write` maps back to `write`, so
|
|
18076
|
+
* `edit` rules round-trip as `write` (a lossy but documented collapse). Unknown
|
|
18077
|
+
* names (e.g. `mcp__github__list_issues`) pass through verbatim.
|
|
18078
|
+
*
|
|
18079
|
+
* @see https://docs.devin.ai/cli/reference/permissions
|
|
18080
|
+
*/
|
|
18081
|
+
const CANONICAL_TO_DEVIN_SCOPE = {
|
|
18082
|
+
read: "Read",
|
|
18083
|
+
write: "Write",
|
|
18084
|
+
edit: "Write",
|
|
18085
|
+
bash: "Exec",
|
|
18086
|
+
webfetch: "Fetch"
|
|
18087
|
+
};
|
|
18088
|
+
/**
|
|
18089
|
+
* Reverse mapping from Devin scope matchers to rulesync canonical names.
|
|
18090
|
+
*/
|
|
18091
|
+
const DEVIN_SCOPE_TO_CANONICAL = {
|
|
18092
|
+
Read: "read",
|
|
18093
|
+
Write: "write",
|
|
18094
|
+
Exec: "bash",
|
|
18095
|
+
Fetch: "webfetch"
|
|
18096
|
+
};
|
|
18097
|
+
function toDevinScope(canonical) {
|
|
18098
|
+
return CANONICAL_TO_DEVIN_SCOPE[canonical] ?? canonical;
|
|
18099
|
+
}
|
|
18100
|
+
function toCanonicalCategory(devinScope) {
|
|
18101
|
+
return DEVIN_SCOPE_TO_CANONICAL[devinScope] ?? devinScope;
|
|
18102
|
+
}
|
|
18103
|
+
/**
|
|
18104
|
+
* Parse a Devin permission entry like `Read(src/**)` into scope and pattern.
|
|
18105
|
+
* Bare entries (e.g. `Read`, or a whole-tool name like `exec`) yield `*`.
|
|
18106
|
+
*/
|
|
18107
|
+
function parseDevinPermissionEntry(entry) {
|
|
18108
|
+
const parenIndex = entry.indexOf("(");
|
|
18109
|
+
if (parenIndex === -1) return {
|
|
18110
|
+
scope: entry,
|
|
18111
|
+
pattern: "*"
|
|
18112
|
+
};
|
|
18113
|
+
const scope = entry.slice(0, parenIndex);
|
|
18114
|
+
if (!entry.endsWith(")")) return {
|
|
18115
|
+
scope,
|
|
18116
|
+
pattern: "*"
|
|
18117
|
+
};
|
|
18118
|
+
return {
|
|
18119
|
+
scope,
|
|
18120
|
+
pattern: entry.slice(parenIndex + 1, -1) || "*"
|
|
18121
|
+
};
|
|
18122
|
+
}
|
|
18123
|
+
/**
|
|
18124
|
+
* Build a Devin permission entry like `Read(src/**)`. A `*` pattern collapses to
|
|
18125
|
+
* the bare scope (`Read`), matching the whole scope.
|
|
18126
|
+
*/
|
|
18127
|
+
function buildDevinPermissionEntry(scope, pattern) {
|
|
18128
|
+
if (pattern === "*") return scope;
|
|
18129
|
+
return `${scope}(${pattern})`;
|
|
18130
|
+
}
|
|
18131
|
+
/**
|
|
18132
|
+
* Permissions generator for Devin Local (native `.devin/` configuration).
|
|
18133
|
+
*
|
|
18134
|
+
* Maps rulesync permission actions onto Devin's `permissions` block inside its
|
|
18135
|
+
* native config file — `allow` / `deny` / `ask` arrays of scope matchers
|
|
18136
|
+
* (`Read(glob)`, `Write(glob)`, `Exec(prefix)`, `Fetch(pattern)`, plus
|
|
18137
|
+
* `mcp__server__tool` patterns). Devin evaluates the arrays with strict
|
|
18138
|
+
* precedence: `deny` is checked before `ask`, which is checked before `allow`,
|
|
18139
|
+
* so a deny rule always wins.
|
|
18140
|
+
*
|
|
18141
|
+
* - Project scope: `.devin/config.json`
|
|
18142
|
+
* - Global scope: `~/.config/devin/config.json`
|
|
18143
|
+
*
|
|
18144
|
+
* The config file is shared with the MCP (`mcpServers`) and, in global mode, the
|
|
18145
|
+
* hooks (`hooks`) features, so reads and writes merge into the existing JSON and
|
|
18146
|
+
* the file is never deleted; only the managed `permissions` key is rewritten.
|
|
18147
|
+
*
|
|
18148
|
+
* @see https://docs.devin.ai/cli/reference/permissions
|
|
18149
|
+
*/
|
|
18150
|
+
var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
18151
|
+
constructor(params) {
|
|
18152
|
+
super({
|
|
18153
|
+
...params,
|
|
18154
|
+
fileContent: params.fileContent ?? "{}"
|
|
18155
|
+
});
|
|
18156
|
+
}
|
|
18157
|
+
/**
|
|
18158
|
+
* config.json may carry the MCP/hooks features' keys, so it is never deleted;
|
|
18159
|
+
* only the managed `permissions` key is rewritten.
|
|
18160
|
+
*/
|
|
18161
|
+
isDeletable() {
|
|
18162
|
+
return false;
|
|
18163
|
+
}
|
|
18164
|
+
static getSettablePaths({ global = false } = {}) {
|
|
18165
|
+
if (global) return {
|
|
18166
|
+
relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
|
|
18167
|
+
relativeFilePath: DEVIN_CONFIG_FILE_NAME
|
|
18168
|
+
};
|
|
18169
|
+
return {
|
|
18170
|
+
relativeDirPath: DEVIN_DIR,
|
|
18171
|
+
relativeFilePath: DEVIN_CONFIG_FILE_NAME
|
|
18172
|
+
};
|
|
18173
|
+
}
|
|
18174
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
18175
|
+
const paths = DevinPermissions.getSettablePaths({ global });
|
|
18176
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"permissions\":{}}";
|
|
18177
|
+
return new DevinPermissions({
|
|
18178
|
+
outputRoot,
|
|
18179
|
+
relativeDirPath: paths.relativeDirPath,
|
|
18180
|
+
relativeFilePath: paths.relativeFilePath,
|
|
18181
|
+
fileContent,
|
|
18182
|
+
validate
|
|
18183
|
+
});
|
|
18184
|
+
}
|
|
18185
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, validate = true }) {
|
|
18186
|
+
const paths = DevinPermissions.getSettablePaths({ global });
|
|
18187
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
18188
|
+
const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
|
|
18189
|
+
let settings;
|
|
18190
|
+
try {
|
|
18191
|
+
const parsed = JSON.parse(existingContent);
|
|
18192
|
+
settings = isRecord(parsed) ? parsed : {};
|
|
18193
|
+
} catch (error) {
|
|
18194
|
+
throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
18195
|
+
}
|
|
18196
|
+
const config = rulesyncPermissions.getJson();
|
|
18197
|
+
const { allow, ask, deny } = convertRulesyncToDevinPermissions(config);
|
|
18198
|
+
const managedScopes = new Set(Object.keys(config.permission).map((category) => toDevinScope(category)));
|
|
18199
|
+
const existingPermissions = isRecord(settings.permissions) ? settings.permissions : {};
|
|
18200
|
+
const preserve = (entries) => (entries ?? []).filter((entry) => !managedScopes.has(parseDevinPermissionEntry(entry).scope));
|
|
18201
|
+
const mergedAllow = (0, es_toolkit.uniq)([...preserve(existingPermissions.allow), ...allow].toSorted());
|
|
18202
|
+
const mergedAsk = (0, es_toolkit.uniq)([...preserve(existingPermissions.ask), ...ask].toSorted());
|
|
18203
|
+
const mergedDeny = (0, es_toolkit.uniq)([...preserve(existingPermissions.deny), ...deny].toSorted());
|
|
18204
|
+
const mergedPermissions = { ...existingPermissions };
|
|
18205
|
+
if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
|
|
18206
|
+
else delete mergedPermissions.allow;
|
|
18207
|
+
if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
|
|
18208
|
+
else delete mergedPermissions.ask;
|
|
18209
|
+
if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
|
|
18210
|
+
else delete mergedPermissions.deny;
|
|
18211
|
+
const merged = {
|
|
18212
|
+
...settings,
|
|
18213
|
+
permissions: mergedPermissions
|
|
18214
|
+
};
|
|
18215
|
+
return new DevinPermissions({
|
|
18216
|
+
outputRoot,
|
|
18217
|
+
relativeDirPath: paths.relativeDirPath,
|
|
18218
|
+
relativeFilePath: paths.relativeFilePath,
|
|
18219
|
+
fileContent: JSON.stringify(merged, null, 2),
|
|
18220
|
+
validate
|
|
18221
|
+
});
|
|
18222
|
+
}
|
|
18223
|
+
toRulesyncPermissions() {
|
|
18224
|
+
let settings;
|
|
18225
|
+
try {
|
|
18226
|
+
const parsed = JSON.parse(this.getFileContent());
|
|
18227
|
+
settings = isRecord(parsed) ? parsed : {};
|
|
18228
|
+
} catch (error) {
|
|
18229
|
+
throw new Error(`Failed to parse Devin permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18230
|
+
}
|
|
18231
|
+
const permissions = isRecord(settings.permissions) ? settings.permissions : {};
|
|
18232
|
+
const config = convertDevinToRulesyncPermissions({
|
|
18233
|
+
allow: Array.isArray(permissions.allow) ? permissions.allow : [],
|
|
18234
|
+
ask: Array.isArray(permissions.ask) ? permissions.ask : [],
|
|
18235
|
+
deny: Array.isArray(permissions.deny) ? permissions.deny : []
|
|
18236
|
+
});
|
|
18237
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
|
|
18238
|
+
}
|
|
18239
|
+
validate() {
|
|
18240
|
+
return {
|
|
18241
|
+
success: true,
|
|
18242
|
+
error: null
|
|
18243
|
+
};
|
|
18244
|
+
}
|
|
18245
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
18246
|
+
return new DevinPermissions({
|
|
18247
|
+
outputRoot,
|
|
18248
|
+
relativeDirPath,
|
|
18249
|
+
relativeFilePath,
|
|
18250
|
+
fileContent: JSON.stringify({ permissions: {} }, null, 2),
|
|
18251
|
+
validate: false
|
|
18252
|
+
});
|
|
18253
|
+
}
|
|
18254
|
+
};
|
|
18255
|
+
/**
|
|
18256
|
+
* Convert rulesync permissions config to Devin allow/ask/deny arrays.
|
|
18257
|
+
*/
|
|
18258
|
+
function convertRulesyncToDevinPermissions(config) {
|
|
18259
|
+
const allow = [];
|
|
18260
|
+
const ask = [];
|
|
18261
|
+
const deny = [];
|
|
18262
|
+
for (const [category, rules] of Object.entries(config.permission)) {
|
|
18263
|
+
const scope = toDevinScope(category);
|
|
18264
|
+
for (const [pattern, action] of Object.entries(rules)) {
|
|
18265
|
+
const entry = buildDevinPermissionEntry(scope, pattern);
|
|
18266
|
+
switch (action) {
|
|
18267
|
+
case "allow":
|
|
18268
|
+
allow.push(entry);
|
|
18269
|
+
break;
|
|
18270
|
+
case "ask":
|
|
18271
|
+
ask.push(entry);
|
|
18272
|
+
break;
|
|
18273
|
+
case "deny":
|
|
18274
|
+
deny.push(entry);
|
|
18275
|
+
break;
|
|
18276
|
+
}
|
|
18277
|
+
}
|
|
18278
|
+
}
|
|
18279
|
+
return {
|
|
18280
|
+
allow,
|
|
18281
|
+
ask,
|
|
18282
|
+
deny
|
|
18283
|
+
};
|
|
18284
|
+
}
|
|
18285
|
+
/**
|
|
18286
|
+
* Convert Devin allow/ask/deny arrays to rulesync permissions config. Entries
|
|
18287
|
+
* are applied allow → ask → deny so the most restrictive action wins for a
|
|
18288
|
+
* given (scope, pattern), mirroring Devin's deny > ask > allow precedence.
|
|
18289
|
+
*/
|
|
18290
|
+
function convertDevinToRulesyncPermissions(params) {
|
|
18291
|
+
const permission = {};
|
|
18292
|
+
const processEntries = (entries, action) => {
|
|
18293
|
+
for (const entry of entries) {
|
|
18294
|
+
const { scope, pattern } = parseDevinPermissionEntry(entry);
|
|
18295
|
+
if (isPrototypePollutionKey(scope) || isPrototypePollutionKey(pattern)) continue;
|
|
18296
|
+
const canonical = toCanonicalCategory(scope);
|
|
18297
|
+
(permission[canonical] ??= {})[pattern] = action;
|
|
18298
|
+
}
|
|
18299
|
+
};
|
|
18300
|
+
processEntries(params.allow, "allow");
|
|
18301
|
+
processEntries(params.ask, "ask");
|
|
18302
|
+
processEntries(params.deny, "deny");
|
|
18303
|
+
return { permission };
|
|
18304
|
+
}
|
|
18305
|
+
//#endregion
|
|
16693
18306
|
//#region src/features/permissions/factorydroid-permissions.ts
|
|
16694
18307
|
/**
|
|
16695
18308
|
* Permissions adapter for Factory Droid.
|
|
@@ -16949,7 +18562,7 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
|
|
|
16949
18562
|
} catch (error) {
|
|
16950
18563
|
throw new Error(`Failed to parse existing Goose permission.yaml at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
16951
18564
|
}
|
|
16952
|
-
const config = isRecord
|
|
18565
|
+
const config = isRecord(parsed) ? { ...parsed } : {};
|
|
16953
18566
|
config[GOOSE_USER_KEY] = convertRulesyncToGoosePermissionConfig({
|
|
16954
18567
|
config: rulesyncPermissions.getJson(),
|
|
16955
18568
|
logger
|
|
@@ -16971,8 +18584,8 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
|
|
|
16971
18584
|
} catch (error) {
|
|
16972
18585
|
throw new Error(`Failed to parse Goose permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
16973
18586
|
}
|
|
16974
|
-
const config = isRecord
|
|
16975
|
-
const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord
|
|
18587
|
+
const config = isRecord(parsed) ? parsed : {};
|
|
18588
|
+
const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
|
|
16976
18589
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
16977
18590
|
}
|
|
16978
18591
|
validate() {
|
|
@@ -17112,7 +18725,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
17112
18725
|
throw new Error(`Failed to parse existing Grok config.toml at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
17113
18726
|
}
|
|
17114
18727
|
const mode = deriveGrokPermissionMode(rulesyncPermissions.getJson());
|
|
17115
|
-
const existingUi = isRecord
|
|
18728
|
+
const existingUi = isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
|
|
17116
18729
|
parsed[GROKCLI_UI_KEY] = {
|
|
17117
18730
|
...existingUi,
|
|
17118
18731
|
[GROKCLI_PERMISSION_MODE_KEY]: mode
|
|
@@ -17134,7 +18747,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
17134
18747
|
} catch (error) {
|
|
17135
18748
|
throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
17136
18749
|
}
|
|
17137
|
-
const action = (isRecord
|
|
18750
|
+
const action = (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
|
|
17138
18751
|
const rulesyncConfig = { permission: { bash: { [CATCH_ALL_PATTERN$2]: action } } };
|
|
17139
18752
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
17140
18753
|
}
|
|
@@ -17216,6 +18829,205 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
17216
18829
|
}
|
|
17217
18830
|
};
|
|
17218
18831
|
//#endregion
|
|
18832
|
+
//#region src/features/permissions/junie-permissions.ts
|
|
18833
|
+
/**
|
|
18834
|
+
* JetBrains Junie CLI Action Allowlist (`allowlist.json`).
|
|
18835
|
+
*
|
|
18836
|
+
* Junie gates actions through an allowlist evaluated top-to-bottom (first match
|
|
18837
|
+
* wins). Project scope lives in `.junie/allowlist.json`; user scope lives in
|
|
18838
|
+
* `~/.junie/allowlist.json`.
|
|
18839
|
+
*
|
|
18840
|
+
* ```json
|
|
18841
|
+
* {
|
|
18842
|
+
* "defaultBehavior": "ask",
|
|
18843
|
+
* "allowReadonlyCommands": true,
|
|
18844
|
+
* "rules": {
|
|
18845
|
+
* "executables": [ { "prefix": "git ", "action": "allow" } ],
|
|
18846
|
+
* "fileEditing": [ { "pattern": "src/**", "action": "allow" } ],
|
|
18847
|
+
* "mcpTools": [ { "prefix": "search", "action": "allow" } ],
|
|
18848
|
+
* "readOutsideProject":[ { "pattern": "/etc/**", "action": "deny" } ]
|
|
18849
|
+
* }
|
|
18850
|
+
* }
|
|
18851
|
+
* ```
|
|
18852
|
+
*
|
|
18853
|
+
* Each rule carries a literal `prefix` (matches commands that start with it) or
|
|
18854
|
+
* a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action`
|
|
18855
|
+
* (`allow` | `ask` | `deny`). rulesync's canonical actions map 1:1 onto Junie's.
|
|
18856
|
+
*
|
|
18857
|
+
* Category mapping (rulesync canonical <-> Junie rule group):
|
|
18858
|
+
* - `bash` <-> `executables`
|
|
18859
|
+
* - `edit`/`write` -> `fileEditing` (imported back as `edit`)
|
|
18860
|
+
* - `read` <-> `readOutsideProject`
|
|
18861
|
+
* - `mcp` <-> `mcpTools`
|
|
18862
|
+
*
|
|
18863
|
+
* Categories Junie cannot represent (e.g. `webfetch`) are skipped on export
|
|
18864
|
+
* (with a warning when they carry rules). The top-level `defaultBehavior` and
|
|
18865
|
+
* `allowReadonlyCommands` settings have no canonical equivalent: they are
|
|
18866
|
+
* preserved verbatim on export but not imported into the rulesync model.
|
|
18867
|
+
*
|
|
18868
|
+
* @see https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html
|
|
18869
|
+
*/
|
|
18870
|
+
const JUNIE_RULE_GROUPS = [
|
|
18871
|
+
"executables",
|
|
18872
|
+
"fileEditing",
|
|
18873
|
+
"mcpTools",
|
|
18874
|
+
"readOutsideProject"
|
|
18875
|
+
];
|
|
18876
|
+
const CANONICAL_TO_JUNIE_GROUP = {
|
|
18877
|
+
bash: "executables",
|
|
18878
|
+
edit: "fileEditing",
|
|
18879
|
+
write: "fileEditing",
|
|
18880
|
+
read: "readOutsideProject",
|
|
18881
|
+
mcp: "mcpTools"
|
|
18882
|
+
};
|
|
18883
|
+
const JUNIE_GROUP_TO_CANONICAL = {
|
|
18884
|
+
executables: "bash",
|
|
18885
|
+
fileEditing: "edit",
|
|
18886
|
+
mcpTools: "mcp",
|
|
18887
|
+
readOutsideProject: "read"
|
|
18888
|
+
};
|
|
18889
|
+
const JUNIE_DEFAULT_BEHAVIOR = "ask";
|
|
18890
|
+
function isPermissionAction$1(value) {
|
|
18891
|
+
return PermissionActionSchema.safeParse(value).success;
|
|
18892
|
+
}
|
|
18893
|
+
/**
|
|
18894
|
+
* Whether a rulesync pattern uses glob syntax. Junie expresses literal
|
|
18895
|
+
* "starts-with" matches as `prefix` and glob matches as `pattern`, so a pattern
|
|
18896
|
+
* containing any glob metacharacter (`*`, `?`, `[`) is emitted as `pattern`.
|
|
18897
|
+
*/
|
|
18898
|
+
function isGlobPattern(pattern) {
|
|
18899
|
+
return /[*?[]/.test(pattern);
|
|
18900
|
+
}
|
|
18901
|
+
var JuniePermissions = class JuniePermissions extends ToolPermissions {
|
|
18902
|
+
constructor(params) {
|
|
18903
|
+
super({
|
|
18904
|
+
...params,
|
|
18905
|
+
fileContent: params.fileContent ?? "{}"
|
|
18906
|
+
});
|
|
18907
|
+
}
|
|
18908
|
+
isDeletable() {
|
|
18909
|
+
return false;
|
|
18910
|
+
}
|
|
18911
|
+
static getSettablePaths(_options = {}) {
|
|
18912
|
+
return {
|
|
18913
|
+
relativeDirPath: JUNIE_DIR,
|
|
18914
|
+
relativeFilePath: JUNIE_PERMISSIONS_FILE_NAME
|
|
18915
|
+
};
|
|
18916
|
+
}
|
|
18917
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
18918
|
+
const paths = JuniePermissions.getSettablePaths({ global });
|
|
18919
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
|
|
18920
|
+
return new JuniePermissions({
|
|
18921
|
+
outputRoot,
|
|
18922
|
+
relativeDirPath: paths.relativeDirPath,
|
|
18923
|
+
relativeFilePath: paths.relativeFilePath,
|
|
18924
|
+
fileContent,
|
|
18925
|
+
validate
|
|
18926
|
+
});
|
|
18927
|
+
}
|
|
18928
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
18929
|
+
const paths = JuniePermissions.getSettablePaths({ global });
|
|
18930
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
18931
|
+
const existingContent = await readOrInitializeFileContent(filePath, "{}");
|
|
18932
|
+
let existing;
|
|
18933
|
+
try {
|
|
18934
|
+
const parsed = JSON.parse(existingContent);
|
|
18935
|
+
existing = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
18936
|
+
} catch (error) {
|
|
18937
|
+
throw new Error(`Failed to parse existing Junie allowlist at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
18938
|
+
}
|
|
18939
|
+
const rules = convertRulesyncToJunieRules({
|
|
18940
|
+
config: rulesyncPermissions.getJson(),
|
|
18941
|
+
logger
|
|
18942
|
+
});
|
|
18943
|
+
const merged = {
|
|
18944
|
+
...existing,
|
|
18945
|
+
defaultBehavior: existing.defaultBehavior ?? JUNIE_DEFAULT_BEHAVIOR,
|
|
18946
|
+
rules
|
|
18947
|
+
};
|
|
18948
|
+
return new JuniePermissions({
|
|
18949
|
+
outputRoot,
|
|
18950
|
+
relativeDirPath: paths.relativeDirPath,
|
|
18951
|
+
relativeFilePath: paths.relativeFilePath,
|
|
18952
|
+
fileContent: JSON.stringify(merged, null, 2),
|
|
18953
|
+
validate: true
|
|
18954
|
+
});
|
|
18955
|
+
}
|
|
18956
|
+
toRulesyncPermissions() {
|
|
18957
|
+
let allowlist;
|
|
18958
|
+
try {
|
|
18959
|
+
const parsed = JSON.parse(this.getFileContent());
|
|
18960
|
+
allowlist = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
18961
|
+
} catch (error) {
|
|
18962
|
+
throw new Error(`Failed to parse Junie permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18963
|
+
}
|
|
18964
|
+
const config = convertJunieToRulesyncPermissions({ allowlist });
|
|
18965
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
|
|
18966
|
+
}
|
|
18967
|
+
validate() {
|
|
18968
|
+
return {
|
|
18969
|
+
success: true,
|
|
18970
|
+
error: null
|
|
18971
|
+
};
|
|
18972
|
+
}
|
|
18973
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
18974
|
+
return new JuniePermissions({
|
|
18975
|
+
outputRoot,
|
|
18976
|
+
relativeDirPath,
|
|
18977
|
+
relativeFilePath,
|
|
18978
|
+
fileContent: "{}",
|
|
18979
|
+
validate: false
|
|
18980
|
+
});
|
|
18981
|
+
}
|
|
18982
|
+
};
|
|
18983
|
+
/**
|
|
18984
|
+
* Convert rulesync permissions config into Junie's `rules` object. Categories
|
|
18985
|
+
* with no Junie rule group (e.g. `webfetch`) are skipped, with a warning when
|
|
18986
|
+
* they carry any rule so the gap is surfaced.
|
|
18987
|
+
*/
|
|
18988
|
+
function convertRulesyncToJunieRules({ config, logger }) {
|
|
18989
|
+
const rules = {};
|
|
18990
|
+
for (const [category, patterns] of Object.entries(config.permission)) {
|
|
18991
|
+
const group = CANONICAL_TO_JUNIE_GROUP[category];
|
|
18992
|
+
if (!group) {
|
|
18993
|
+
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.`);
|
|
18994
|
+
continue;
|
|
18995
|
+
}
|
|
18996
|
+
for (const [pattern, action] of Object.entries(patterns)) {
|
|
18997
|
+
const rule = isGlobPattern(pattern) ? {
|
|
18998
|
+
pattern,
|
|
18999
|
+
action
|
|
19000
|
+
} : {
|
|
19001
|
+
prefix: pattern,
|
|
19002
|
+
action
|
|
19003
|
+
};
|
|
19004
|
+
(rules[group] ??= []).push(rule);
|
|
19005
|
+
}
|
|
19006
|
+
}
|
|
19007
|
+
return rules;
|
|
19008
|
+
}
|
|
19009
|
+
/**
|
|
19010
|
+
* Convert a Junie allowlist back into rulesync permissions config. The
|
|
19011
|
+
* top-level `defaultBehavior` / `allowReadonlyCommands` settings have no
|
|
19012
|
+
* canonical equivalent and are not imported.
|
|
19013
|
+
*/
|
|
19014
|
+
function convertJunieToRulesyncPermissions({ allowlist }) {
|
|
19015
|
+
const permission = {};
|
|
19016
|
+
const rules = allowlist.rules;
|
|
19017
|
+
if (rules && typeof rules === "object") for (const group of JUNIE_RULE_GROUPS) {
|
|
19018
|
+
const list = rules[group];
|
|
19019
|
+
if (!Array.isArray(list)) continue;
|
|
19020
|
+
const category = JUNIE_GROUP_TO_CANONICAL[group];
|
|
19021
|
+
for (const rule of list) {
|
|
19022
|
+
if (!rule || typeof rule !== "object") continue;
|
|
19023
|
+
const pattern = typeof rule.pattern === "string" ? rule.pattern : typeof rule.prefix === "string" ? rule.prefix : void 0;
|
|
19024
|
+
if (pattern === void 0 || !isPermissionAction$1(rule.action)) continue;
|
|
19025
|
+
(permission[category] ??= {})[pattern] = rule.action;
|
|
19026
|
+
}
|
|
19027
|
+
}
|
|
19028
|
+
return { permission };
|
|
19029
|
+
}
|
|
19030
|
+
//#endregion
|
|
17219
19031
|
//#region src/features/permissions/kilo-permissions.ts
|
|
17220
19032
|
const KiloPermissionSchema = zod_mini.z.union([zod_mini.z.enum([
|
|
17221
19033
|
"allow",
|
|
@@ -17564,7 +19376,11 @@ const OpencodePermissionSchema = zod_mini.z.union([zod_mini.z.enum([
|
|
|
17564
19376
|
"ask",
|
|
17565
19377
|
"deny"
|
|
17566
19378
|
]))]);
|
|
17567
|
-
const OpencodePermissionsConfigSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.
|
|
19379
|
+
const OpencodePermissionsConfigSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.enum([
|
|
19380
|
+
"allow",
|
|
19381
|
+
"ask",
|
|
19382
|
+
"deny"
|
|
19383
|
+
]), zod_mini.z.record(zod_mini.z.string(), OpencodePermissionSchema)])) });
|
|
17568
19384
|
var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
17569
19385
|
json;
|
|
17570
19386
|
constructor(params) {
|
|
@@ -17667,6 +19483,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
17667
19483
|
}
|
|
17668
19484
|
normalizePermission(permission) {
|
|
17669
19485
|
if (!permission) return {};
|
|
19486
|
+
if (typeof permission === "string") return { "*": { "*": permission } };
|
|
17670
19487
|
return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
|
|
17671
19488
|
}
|
|
17672
19489
|
};
|
|
@@ -17703,7 +19520,7 @@ const QWEN_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL
|
|
|
17703
19520
|
function toQwenToolName(canonical) {
|
|
17704
19521
|
return CANONICAL_TO_QWEN_TOOL_NAMES[canonical] ?? canonical;
|
|
17705
19522
|
}
|
|
17706
|
-
function toCanonicalToolName$
|
|
19523
|
+
function toCanonicalToolName$3(qwenName) {
|
|
17707
19524
|
return QWEN_TO_CANONICAL_TOOL_NAMES[qwenName] ?? qwenName;
|
|
17708
19525
|
}
|
|
17709
19526
|
function parseQwenPermissionEntry(entry, options = {}) {
|
|
@@ -17897,13 +19714,242 @@ function convertQwenToRulesyncPermissions(params) {
|
|
|
17897
19714
|
const parsed = parseQwenPermissionEntry(entry, { logger });
|
|
17898
19715
|
if (!parsed.ok) {
|
|
17899
19716
|
if (action === "deny") {
|
|
17900
|
-
const canonical = toCanonicalToolName$
|
|
19717
|
+
const canonical = toCanonicalToolName$3(parsed.toolName);
|
|
17901
19718
|
if (!permission[canonical]) permission[canonical] = {};
|
|
17902
19719
|
permission[canonical]["*"] = action;
|
|
17903
19720
|
}
|
|
17904
19721
|
continue;
|
|
17905
19722
|
}
|
|
17906
19723
|
const { toolName, pattern } = parsed;
|
|
19724
|
+
const canonical = toCanonicalToolName$3(toolName);
|
|
19725
|
+
if (!permission[canonical]) permission[canonical] = {};
|
|
19726
|
+
permission[canonical][pattern] = action;
|
|
19727
|
+
}
|
|
19728
|
+
};
|
|
19729
|
+
processEntries(params.allow, "allow");
|
|
19730
|
+
processEntries(params.ask, "ask");
|
|
19731
|
+
processEntries(params.deny, "deny");
|
|
19732
|
+
return { permission };
|
|
19733
|
+
}
|
|
19734
|
+
//#endregion
|
|
19735
|
+
//#region src/features/permissions/reasonix-permissions.ts
|
|
19736
|
+
/**
|
|
19737
|
+
* Mapping from rulesync canonical tool category names (lowercase) to Reasonix
|
|
19738
|
+
* permission-rule tool families (PascalCase).
|
|
19739
|
+
*
|
|
19740
|
+
* Reasonix's `[permissions]` rule syntax (SPEC.md §3.7) is explicitly
|
|
19741
|
+
* documented as "Claude Code-style": "Bash and file mutation approvals use
|
|
19742
|
+
* Claude Code-style families such as `Bash(npm run build)`, `Bash(npm run
|
|
19743
|
+
* test:*)`, and `Edit(docs/**)`." Reasonix also accepts legacy lowercase tool
|
|
19744
|
+
* IDs for compatibility, but new rules are saved using these PascalCase
|
|
19745
|
+
* families, so rulesync reuses the same mapping `claudecode-permissions.ts`
|
|
19746
|
+
* uses (the closest documented precedent for this syntax).
|
|
19747
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
|
|
19748
|
+
*/
|
|
19749
|
+
const CANONICAL_TO_REASONIX_TOOL_NAMES = {
|
|
19750
|
+
bash: "Bash",
|
|
19751
|
+
read: "Read",
|
|
19752
|
+
edit: "Edit",
|
|
19753
|
+
write: "Write",
|
|
19754
|
+
webfetch: "WebFetch",
|
|
19755
|
+
websearch: "WebSearch",
|
|
19756
|
+
grep: "Grep",
|
|
19757
|
+
glob: "Glob",
|
|
19758
|
+
notebookedit: "NotebookEdit",
|
|
19759
|
+
agent: "Agent"
|
|
19760
|
+
};
|
|
19761
|
+
/**
|
|
19762
|
+
* Reverse mapping from Reasonix tool names to rulesync canonical names.
|
|
19763
|
+
*/
|
|
19764
|
+
const REASONIX_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
19765
|
+
function toReasonixToolName(canonical) {
|
|
19766
|
+
return CANONICAL_TO_REASONIX_TOOL_NAMES[canonical] ?? canonical;
|
|
19767
|
+
}
|
|
19768
|
+
function toCanonicalToolName$2(reasonixName) {
|
|
19769
|
+
return REASONIX_TO_CANONICAL_TOOL_NAMES[reasonixName] ?? reasonixName;
|
|
19770
|
+
}
|
|
19771
|
+
/**
|
|
19772
|
+
* Parse a Reasonix permission entry like "Bash(npm run *)" into tool name and pattern.
|
|
19773
|
+
* If no parentheses, returns the tool name with "*" as the pattern.
|
|
19774
|
+
*/
|
|
19775
|
+
function parseReasonixPermissionEntry(entry) {
|
|
19776
|
+
const parenIndex = entry.indexOf("(");
|
|
19777
|
+
if (parenIndex === -1) return {
|
|
19778
|
+
toolName: entry,
|
|
19779
|
+
pattern: "*"
|
|
19780
|
+
};
|
|
19781
|
+
const toolName = entry.slice(0, parenIndex);
|
|
19782
|
+
if (!entry.endsWith(")")) return {
|
|
19783
|
+
toolName,
|
|
19784
|
+
pattern: "*"
|
|
19785
|
+
};
|
|
19786
|
+
return {
|
|
19787
|
+
toolName,
|
|
19788
|
+
pattern: entry.slice(parenIndex + 1, -1) || "*"
|
|
19789
|
+
};
|
|
19790
|
+
}
|
|
19791
|
+
/**
|
|
19792
|
+
* Build a Reasonix permission entry like "Bash(npm run *)".
|
|
19793
|
+
* If the pattern is "*", returns just the tool name.
|
|
19794
|
+
*/
|
|
19795
|
+
function buildReasonixPermissionEntry(toolName, pattern) {
|
|
19796
|
+
if (pattern === "*") return toolName;
|
|
19797
|
+
return `${toolName}(${pattern})`;
|
|
19798
|
+
}
|
|
19799
|
+
function parseReasonixConfig(fileContent) {
|
|
19800
|
+
const parsed = smol_toml.parse(fileContent || smol_toml.stringify({}));
|
|
19801
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
19802
|
+
return { ...parsed };
|
|
19803
|
+
}
|
|
19804
|
+
function toStringArray$1(value) {
|
|
19805
|
+
if (!Array.isArray(value)) return [];
|
|
19806
|
+
return value.filter((entry) => typeof entry === "string");
|
|
19807
|
+
}
|
|
19808
|
+
function toPermissionsTable(value) {
|
|
19809
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
19810
|
+
return { ...value };
|
|
19811
|
+
}
|
|
19812
|
+
var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
19813
|
+
toml;
|
|
19814
|
+
constructor(params) {
|
|
19815
|
+
super(params);
|
|
19816
|
+
this.toml = parseReasonixConfig(this.getFileContent());
|
|
19817
|
+
}
|
|
19818
|
+
isDeletable() {
|
|
19819
|
+
return false;
|
|
19820
|
+
}
|
|
19821
|
+
static getSettablePaths({ global } = {}) {
|
|
19822
|
+
if (global) return {
|
|
19823
|
+
relativeDirPath: REASONIX_GLOBAL_DIR,
|
|
19824
|
+
relativeFilePath: REASONIX_GLOBAL_PERMISSIONS_FILE_NAME
|
|
19825
|
+
};
|
|
19826
|
+
return {
|
|
19827
|
+
relativeDirPath: ".",
|
|
19828
|
+
relativeFilePath: REASONIX_PROJECT_PERMISSIONS_FILE_NAME
|
|
19829
|
+
};
|
|
19830
|
+
}
|
|
19831
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
19832
|
+
const paths = this.getSettablePaths({ global });
|
|
19833
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({});
|
|
19834
|
+
return new ReasonixPermissions({
|
|
19835
|
+
outputRoot,
|
|
19836
|
+
relativeDirPath: paths.relativeDirPath,
|
|
19837
|
+
relativeFilePath: paths.relativeFilePath,
|
|
19838
|
+
fileContent,
|
|
19839
|
+
validate
|
|
19840
|
+
});
|
|
19841
|
+
}
|
|
19842
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
|
|
19843
|
+
const paths = this.getSettablePaths({ global });
|
|
19844
|
+
const parsed = parseReasonixConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
|
|
19845
|
+
const config = rulesyncPermissions.getJson();
|
|
19846
|
+
const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
|
|
19847
|
+
const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
|
|
19848
|
+
const existingPermissions = toPermissionsTable(parsed.permissions);
|
|
19849
|
+
const preservedAllow = toStringArray$1(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
|
|
19850
|
+
const preservedAsk = toStringArray$1(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
|
|
19851
|
+
const preservedDeny = toStringArray$1(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
|
|
19852
|
+
if (logger && managedToolNames.has("Read")) {
|
|
19853
|
+
const droppedReadDenyEntries = toStringArray$1(existingPermissions.deny).filter((entry) => {
|
|
19854
|
+
const { toolName } = parseReasonixPermissionEntry(entry);
|
|
19855
|
+
return toolName === "Read";
|
|
19856
|
+
});
|
|
19857
|
+
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.`);
|
|
19858
|
+
}
|
|
19859
|
+
const mergedPermissions = { ...existingPermissions };
|
|
19860
|
+
const mergedAllow = (0, es_toolkit.uniq)([...preservedAllow, ...allow].toSorted());
|
|
19861
|
+
const mergedAsk = (0, es_toolkit.uniq)([...preservedAsk, ...ask].toSorted());
|
|
19862
|
+
const mergedDeny = (0, es_toolkit.uniq)([...preservedDeny, ...deny].toSorted());
|
|
19863
|
+
if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
|
|
19864
|
+
else delete mergedPermissions.allow;
|
|
19865
|
+
if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
|
|
19866
|
+
else delete mergedPermissions.ask;
|
|
19867
|
+
if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
|
|
19868
|
+
else delete mergedPermissions.deny;
|
|
19869
|
+
const merged = {
|
|
19870
|
+
...parsed,
|
|
19871
|
+
permissions: mergedPermissions
|
|
19872
|
+
};
|
|
19873
|
+
const fileContent = smol_toml.stringify(merged);
|
|
19874
|
+
return new ReasonixPermissions({
|
|
19875
|
+
outputRoot,
|
|
19876
|
+
relativeDirPath: paths.relativeDirPath,
|
|
19877
|
+
relativeFilePath: paths.relativeFilePath,
|
|
19878
|
+
fileContent,
|
|
19879
|
+
validate
|
|
19880
|
+
});
|
|
19881
|
+
}
|
|
19882
|
+
toRulesyncPermissions() {
|
|
19883
|
+
const permissions = toPermissionsTable(this.toml.permissions);
|
|
19884
|
+
const config = convertReasonixToRulesyncPermissions({
|
|
19885
|
+
allow: toStringArray$1(permissions.allow),
|
|
19886
|
+
ask: toStringArray$1(permissions.ask),
|
|
19887
|
+
deny: toStringArray$1(permissions.deny)
|
|
19888
|
+
});
|
|
19889
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
|
|
19890
|
+
}
|
|
19891
|
+
validate() {
|
|
19892
|
+
try {
|
|
19893
|
+
parseReasonixConfig(this.getFileContent());
|
|
19894
|
+
return {
|
|
19895
|
+
success: true,
|
|
19896
|
+
error: null
|
|
19897
|
+
};
|
|
19898
|
+
} catch (error) {
|
|
19899
|
+
return {
|
|
19900
|
+
success: false,
|
|
19901
|
+
error: /* @__PURE__ */ new Error(`Failed to parse Reasonix config TOML: ${formatError(error)}`)
|
|
19902
|
+
};
|
|
19903
|
+
}
|
|
19904
|
+
}
|
|
19905
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
19906
|
+
return new ReasonixPermissions({
|
|
19907
|
+
outputRoot,
|
|
19908
|
+
relativeDirPath,
|
|
19909
|
+
relativeFilePath,
|
|
19910
|
+
fileContent: smol_toml.stringify({}),
|
|
19911
|
+
validate: false
|
|
19912
|
+
});
|
|
19913
|
+
}
|
|
19914
|
+
};
|
|
19915
|
+
/**
|
|
19916
|
+
* Convert rulesync permissions config to Reasonix allow/ask/deny arrays.
|
|
19917
|
+
*/
|
|
19918
|
+
function convertRulesyncToReasonixPermissions(config) {
|
|
19919
|
+
const allow = [];
|
|
19920
|
+
const ask = [];
|
|
19921
|
+
const deny = [];
|
|
19922
|
+
for (const [category, rules] of Object.entries(config.permission)) {
|
|
19923
|
+
const reasonixToolName = toReasonixToolName(category);
|
|
19924
|
+
for (const [pattern, action] of Object.entries(rules)) {
|
|
19925
|
+
const entry = buildReasonixPermissionEntry(reasonixToolName, pattern);
|
|
19926
|
+
switch (action) {
|
|
19927
|
+
case "allow":
|
|
19928
|
+
allow.push(entry);
|
|
19929
|
+
break;
|
|
19930
|
+
case "ask":
|
|
19931
|
+
ask.push(entry);
|
|
19932
|
+
break;
|
|
19933
|
+
case "deny":
|
|
19934
|
+
deny.push(entry);
|
|
19935
|
+
break;
|
|
19936
|
+
}
|
|
19937
|
+
}
|
|
19938
|
+
}
|
|
19939
|
+
return {
|
|
19940
|
+
allow,
|
|
19941
|
+
ask,
|
|
19942
|
+
deny
|
|
19943
|
+
};
|
|
19944
|
+
}
|
|
19945
|
+
/**
|
|
19946
|
+
* Convert Reasonix allow/ask/deny arrays to rulesync permissions config.
|
|
19947
|
+
*/
|
|
19948
|
+
function convertReasonixToRulesyncPermissions(params) {
|
|
19949
|
+
const permission = {};
|
|
19950
|
+
const processEntries = (entries, action) => {
|
|
19951
|
+
for (const entry of entries) {
|
|
19952
|
+
const { toolName, pattern } = parseReasonixPermissionEntry(entry);
|
|
17907
19953
|
const canonical = toCanonicalToolName$2(toolName);
|
|
17908
19954
|
if (!permission[canonical]) permission[canonical] = {};
|
|
17909
19955
|
permission[canonical][pattern] = action;
|
|
@@ -18016,13 +20062,13 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
18016
20062
|
} catch (error) {
|
|
18017
20063
|
throw new Error(`Failed to parse existing Rovodev config at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
18018
20064
|
}
|
|
18019
|
-
const config = isRecord
|
|
20065
|
+
const config = isRecord(parsed) ? { ...parsed } : {};
|
|
18020
20066
|
const toolPermissions = convertRulesyncToRovodevToolPermissions({
|
|
18021
20067
|
config: rulesyncPermissions.getJson(),
|
|
18022
20068
|
logger
|
|
18023
20069
|
});
|
|
18024
20070
|
config.toolPermissions = {
|
|
18025
|
-
...isRecord
|
|
20071
|
+
...isRecord(config.toolPermissions) ? { ...config.toolPermissions } : {},
|
|
18026
20072
|
...toolPermissions
|
|
18027
20073
|
};
|
|
18028
20074
|
return new RovodevPermissions({
|
|
@@ -18042,8 +20088,8 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
18042
20088
|
} catch (error) {
|
|
18043
20089
|
throw new Error(`Failed to parse Rovodev permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18044
20090
|
}
|
|
18045
|
-
const config = isRecord
|
|
18046
|
-
const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord
|
|
20091
|
+
const config = isRecord(parsed) ? parsed : {};
|
|
20092
|
+
const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord(config.toolPermissions) ? config.toolPermissions : {});
|
|
18047
20093
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
18048
20094
|
}
|
|
18049
20095
|
validate() {
|
|
@@ -18121,11 +20167,11 @@ function convertBashRules(rules) {
|
|
|
18121
20167
|
function convertRovodevToolPermissionsToRulesync(toolPermissions) {
|
|
18122
20168
|
const permission = {};
|
|
18123
20169
|
const bash = toolPermissions.bash;
|
|
18124
|
-
if (isRecord
|
|
20170
|
+
if (isRecord(bash)) {
|
|
18125
20171
|
const bashRules = {};
|
|
18126
20172
|
if (isPermissionAction(bash.default)) bashRules[CATCH_ALL_PATTERN$1] = bash.default;
|
|
18127
20173
|
if (Array.isArray(bash.commands)) {
|
|
18128
|
-
for (const entry of bash.commands) if (isRecord
|
|
20174
|
+
for (const entry of bash.commands) if (isRecord(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
|
|
18129
20175
|
}
|
|
18130
20176
|
if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
|
|
18131
20177
|
}
|
|
@@ -18264,21 +20310,6 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
|
18264
20310
|
}
|
|
18265
20311
|
};
|
|
18266
20312
|
/**
|
|
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
20313
|
* Resolve the active Takt provider: the top-level `provider:` value, else the
|
|
18283
20314
|
* sole key in `provider_profiles`, else the `claude` default.
|
|
18284
20315
|
*/
|
|
@@ -18600,8 +20631,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
18600
20631
|
config: rulesyncPermissions.getJson(),
|
|
18601
20632
|
logger
|
|
18602
20633
|
});
|
|
18603
|
-
const agents = isRecord
|
|
18604
|
-
const profiles = isRecord
|
|
20634
|
+
const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
|
|
20635
|
+
const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
|
|
18605
20636
|
const mergedAllow = (0, es_toolkit.uniq)(allow.toSorted());
|
|
18606
20637
|
const mergedDeny = (0, es_toolkit.uniq)(deny.toSorted());
|
|
18607
20638
|
if (mergedAllow.length > 0) profiles[ALLOWLIST_KEY] = mergedAllow;
|
|
@@ -18626,8 +20657,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
18626
20657
|
} catch (error) {
|
|
18627
20658
|
throw new Error(`Failed to parse Warp permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18628
20659
|
}
|
|
18629
|
-
const agents = isRecord
|
|
18630
|
-
const profiles = isRecord
|
|
20660
|
+
const agents = isRecord(settings.agents) ? settings.agents : {};
|
|
20661
|
+
const profiles = isRecord(agents.profiles) ? agents.profiles : {};
|
|
18631
20662
|
const config = convertWarpToRulesyncPermissions({
|
|
18632
20663
|
allow: isStringArray(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
|
|
18633
20664
|
deny: isStringArray(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
|
|
@@ -18978,6 +21009,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
18978
21009
|
supportsImport: true
|
|
18979
21010
|
}
|
|
18980
21011
|
}],
|
|
21012
|
+
["devin", {
|
|
21013
|
+
class: DevinPermissions,
|
|
21014
|
+
meta: {
|
|
21015
|
+
supportsProject: true,
|
|
21016
|
+
supportsGlobal: true,
|
|
21017
|
+
supportsImport: true
|
|
21018
|
+
}
|
|
21019
|
+
}],
|
|
18981
21020
|
["factorydroid", {
|
|
18982
21021
|
class: FactorydroidPermissions,
|
|
18983
21022
|
meta: {
|
|
@@ -19010,6 +21049,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
19010
21049
|
supportsImport: true
|
|
19011
21050
|
}
|
|
19012
21051
|
}],
|
|
21052
|
+
["junie", {
|
|
21053
|
+
class: JuniePermissions,
|
|
21054
|
+
meta: {
|
|
21055
|
+
supportsProject: true,
|
|
21056
|
+
supportsGlobal: true,
|
|
21057
|
+
supportsImport: true
|
|
21058
|
+
}
|
|
21059
|
+
}],
|
|
19013
21060
|
["kilo", {
|
|
19014
21061
|
class: KiloPermissions,
|
|
19015
21062
|
meta: {
|
|
@@ -19058,6 +21105,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
19058
21105
|
supportsImport: true
|
|
19059
21106
|
}
|
|
19060
21107
|
}],
|
|
21108
|
+
["reasonix", {
|
|
21109
|
+
class: ReasonixPermissions,
|
|
21110
|
+
meta: {
|
|
21111
|
+
supportsProject: true,
|
|
21112
|
+
supportsGlobal: true,
|
|
21113
|
+
supportsImport: true
|
|
21114
|
+
}
|
|
21115
|
+
}],
|
|
19061
21116
|
["rovodev", {
|
|
19062
21117
|
class: RovodevPermissions,
|
|
19063
21118
|
meta: {
|
|
@@ -19610,7 +21665,7 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
19610
21665
|
opencode: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
19611
21666
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
19612
21667
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
19613
|
-
compatibility: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
21668
|
+
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
19614
21669
|
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
19615
21670
|
})),
|
|
19616
21671
|
kilo: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
@@ -19631,7 +21686,8 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
19631
21686
|
})),
|
|
19632
21687
|
copilotcli: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
19633
21688
|
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())]))
|
|
21689
|
+
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
21690
|
+
"argument-hint": zod_mini.z.optional(zod_mini.z.string())
|
|
19635
21691
|
})),
|
|
19636
21692
|
pi: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
19637
21693
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
@@ -21385,7 +23441,11 @@ const CopilotSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
21385
23441
|
});
|
|
21386
23442
|
/**
|
|
21387
23443
|
* Represents a GitHub Copilot skill directory.
|
|
21388
|
-
*
|
|
23444
|
+
*
|
|
23445
|
+
* Copilot discovers project skills from `.github/skills/` and personal/global
|
|
23446
|
+
* skills from `~/.copilot/skills/`. Each skill is a directory containing a
|
|
23447
|
+
* `SKILL.md` file with `name`/`description` frontmatter.
|
|
23448
|
+
* https://docs.github.com/en/copilot/concepts/agents/about-agent-skills
|
|
21389
23449
|
*/
|
|
21390
23450
|
var CopilotSkill = class CopilotSkill extends ToolSkill {
|
|
21391
23451
|
constructor({ outputRoot = process.cwd(), relativeDirPath = COPILOT_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
@@ -21407,7 +23467,7 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
|
|
|
21407
23467
|
}
|
|
21408
23468
|
}
|
|
21409
23469
|
static getSettablePaths(options) {
|
|
21410
|
-
if (options?.global)
|
|
23470
|
+
if (options?.global) return { relativeDirPath: COPILOT_SKILLS_GLOBAL_DIR_PATH };
|
|
21411
23471
|
return { relativeDirPath: COPILOT_SKILLS_DIR_PATH };
|
|
21412
23472
|
}
|
|
21413
23473
|
getFrontmatter() {
|
|
@@ -21522,7 +23582,8 @@ const CopilotcliSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
21522
23582
|
name: zod_mini.z.string(),
|
|
21523
23583
|
description: zod_mini.z.string(),
|
|
21524
23584
|
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())]))
|
|
23585
|
+
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
23586
|
+
"argument-hint": zod_mini.z.optional(zod_mini.z.string())
|
|
21526
23587
|
});
|
|
21527
23588
|
/**
|
|
21528
23589
|
* Represents a GitHub Copilot CLI skill directory.
|
|
@@ -21552,7 +23613,7 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
21552
23613
|
}
|
|
21553
23614
|
}
|
|
21554
23615
|
static getSettablePaths(options) {
|
|
21555
|
-
if (options?.global) return { relativeDirPath:
|
|
23616
|
+
if (options?.global) return { relativeDirPath: COPILOT_SKILLS_GLOBAL_DIR_PATH };
|
|
21556
23617
|
return { relativeDirPath: COPILOT_SKILLS_DIR_PATH };
|
|
21557
23618
|
}
|
|
21558
23619
|
getFrontmatter() {
|
|
@@ -21580,7 +23641,8 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
21580
23641
|
const frontmatter = this.getFrontmatter();
|
|
21581
23642
|
const copilotcliSection = {
|
|
21582
23643
|
...frontmatter.license !== void 0 && { license: frontmatter.license },
|
|
21583
|
-
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
|
|
23644
|
+
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
|
|
23645
|
+
...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] }
|
|
21584
23646
|
};
|
|
21585
23647
|
const rulesyncFrontmatter = {
|
|
21586
23648
|
name: frontmatter.name,
|
|
@@ -21606,7 +23668,8 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
21606
23668
|
name: rulesyncFrontmatter.name,
|
|
21607
23669
|
description: rulesyncFrontmatter.description,
|
|
21608
23670
|
...rulesyncFrontmatter.copilotcli?.license !== void 0 && { license: rulesyncFrontmatter.copilotcli.license },
|
|
21609
|
-
...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] }
|
|
23671
|
+
...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] },
|
|
23672
|
+
...rulesyncFrontmatter.copilotcli?.["argument-hint"] !== void 0 && { "argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"] }
|
|
21610
23673
|
};
|
|
21611
23674
|
return new CopilotcliSkill({
|
|
21612
23675
|
outputRoot,
|
|
@@ -22832,8 +24895,7 @@ var KiroSkill = class KiroSkill extends ToolSkill {
|
|
|
22832
24895
|
if (!result.success) throw result.error;
|
|
22833
24896
|
}
|
|
22834
24897
|
}
|
|
22835
|
-
static getSettablePaths(
|
|
22836
|
-
if (options?.global) throw new Error("KiroSkill does not support global mode.");
|
|
24898
|
+
static getSettablePaths(_options) {
|
|
22837
24899
|
return { relativeDirPath: KIRO_SKILLS_DIR_PATH };
|
|
22838
24900
|
}
|
|
22839
24901
|
getFrontmatter() {
|
|
@@ -22975,10 +25037,19 @@ const OpenCodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
22975
25037
|
name: zod_mini.z.string(),
|
|
22976
25038
|
description: zod_mini.z.string(),
|
|
22977
25039
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
22978
|
-
compatibility: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
25040
|
+
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
22979
25041
|
metadata: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
22980
25042
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
22981
25043
|
});
|
|
25044
|
+
/**
|
|
25045
|
+
* Reads a top-level `compatibility` value from rulesync frontmatter, accepting
|
|
25046
|
+
* both the documented string form (e.g. `compatibility: opencode`) and the
|
|
25047
|
+
* legacy object form. Returns `undefined` for any other shape.
|
|
25048
|
+
*/
|
|
25049
|
+
function readTopLevelCompatibility(value) {
|
|
25050
|
+
if (typeof value === "string") return value;
|
|
25051
|
+
if (typeof value === "object" && value !== null) return value;
|
|
25052
|
+
}
|
|
22982
25053
|
var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
22983
25054
|
constructor({ outputRoot = process.cwd(), relativeDirPath = OPENCODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
22984
25055
|
super({
|
|
@@ -23055,7 +25126,7 @@ var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
|
23055
25126
|
const opencodeSection = rulesyncFrontmatter.opencode;
|
|
23056
25127
|
const looseTopLevel = rulesyncFrontmatter;
|
|
23057
25128
|
const topLevelLicense = typeof looseTopLevel.license === "string" ? looseTopLevel.license : void 0;
|
|
23058
|
-
const topLevelCompatibility =
|
|
25129
|
+
const topLevelCompatibility = readTopLevelCompatibility(looseTopLevel.compatibility);
|
|
23059
25130
|
const topLevelMetadata = typeof looseTopLevel.metadata === "object" && looseTopLevel.metadata !== null ? looseTopLevel.metadata : void 0;
|
|
23060
25131
|
const license = opencodeSection?.license ?? topLevelLicense;
|
|
23061
25132
|
const compatibility = opencodeSection?.compatibility ?? topLevelCompatibility;
|
|
@@ -24409,7 +26480,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
24409
26480
|
meta: {
|
|
24410
26481
|
supportsProject: true,
|
|
24411
26482
|
supportsSimulated: false,
|
|
24412
|
-
supportsGlobal:
|
|
26483
|
+
supportsGlobal: true
|
|
24413
26484
|
}
|
|
24414
26485
|
}],
|
|
24415
26486
|
["copilotcli", {
|
|
@@ -24497,7 +26568,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
24497
26568
|
meta: {
|
|
24498
26569
|
supportsProject: true,
|
|
24499
26570
|
supportsSimulated: false,
|
|
24500
|
-
supportsGlobal:
|
|
26571
|
+
supportsGlobal: true
|
|
24501
26572
|
}
|
|
24502
26573
|
}],
|
|
24503
26574
|
["kiro-ide", {
|
|
@@ -24505,7 +26576,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
24505
26576
|
meta: {
|
|
24506
26577
|
supportsProject: true,
|
|
24507
26578
|
supportsSimulated: false,
|
|
24508
|
-
supportsGlobal:
|
|
26579
|
+
supportsGlobal: true
|
|
24509
26580
|
}
|
|
24510
26581
|
}],
|
|
24511
26582
|
["opencode", {
|
|
@@ -28593,7 +30664,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
28593
30664
|
class: KiroCliSubagent,
|
|
28594
30665
|
meta: {
|
|
28595
30666
|
supportsSimulated: false,
|
|
28596
|
-
supportsGlobal:
|
|
30667
|
+
supportsGlobal: true,
|
|
28597
30668
|
filePattern: "*.json"
|
|
28598
30669
|
}
|
|
28599
30670
|
}],
|
|
@@ -28601,7 +30672,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
28601
30672
|
class: KiroIdeSubagent,
|
|
28602
30673
|
meta: {
|
|
28603
30674
|
supportsSimulated: false,
|
|
28604
|
-
supportsGlobal:
|
|
30675
|
+
supportsGlobal: true,
|
|
28605
30676
|
filePattern: "*.md"
|
|
28606
30677
|
}
|
|
28607
30678
|
}],
|
|
@@ -31138,7 +33209,7 @@ const STRATEGIES = [
|
|
|
31138
33209
|
* plus companion `globs`/`description` fields. (`.devin/rules/` is the
|
|
31139
33210
|
* pre-rebrand legacy location the tool still reads.)
|
|
31140
33211
|
* - Global scope: a single plain-markdown, always-on file (no frontmatter) at
|
|
31141
|
-
* `~/.
|
|
33212
|
+
* `~/.config/devin/AGENTS.md` (Devin Local global always-on rules).
|
|
31142
33213
|
*
|
|
31143
33214
|
* Trigger inference (when no explicit devin trigger is persisted):
|
|
31144
33215
|
* - Specific globs (non wildcard) → glob
|
|
@@ -31165,8 +33236,8 @@ var DevinRule = class DevinRule extends ToolRule {
|
|
|
31165
33236
|
}
|
|
31166
33237
|
static getGlobalRootPath(excludeToolDir) {
|
|
31167
33238
|
return {
|
|
31168
|
-
relativeDirPath: buildToolPath(
|
|
31169
|
-
relativeFilePath:
|
|
33239
|
+
relativeDirPath: buildToolPath(DEVIN_GLOBAL_CONFIG_DIR_PATH, ".", excludeToolDir),
|
|
33240
|
+
relativeFilePath: DEVIN_GLOBAL_AGENTS_FILE_NAME
|
|
31170
33241
|
};
|
|
31171
33242
|
}
|
|
31172
33243
|
static getSettablePaths({ global = false, excludeToolDir } = {}) {
|
|
@@ -31729,6 +33800,9 @@ var KiloRule = class KiloRule extends ToolRule {
|
|
|
31729
33800
|
nonRoot: { relativeDirPath: buildToolPath(KILO_DIR, KILO_RULES_DIR_NAME, excludeToolDir) }
|
|
31730
33801
|
};
|
|
31731
33802
|
}
|
|
33803
|
+
static getExtraSharedWritePaths({ global = false } = {}) {
|
|
33804
|
+
return global ? [] : [KiloMcp.getSettablePaths({ global: false })];
|
|
33805
|
+
}
|
|
31732
33806
|
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
31733
33807
|
const paths = this.getSettablePaths({ global });
|
|
31734
33808
|
if (relativeFilePath === paths.root.relativeFilePath) {
|
|
@@ -31990,6 +34064,9 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
|
|
|
31990
34064
|
nonRoot: { relativeDirPath: buildToolPath(OPENCODE_DIR, "memories", excludeToolDir) }
|
|
31991
34065
|
};
|
|
31992
34066
|
}
|
|
34067
|
+
static getExtraSharedWritePaths({ global = false } = {}) {
|
|
34068
|
+
return global ? [] : [OpencodeMcp.getSettablePaths({ global: false })];
|
|
34069
|
+
}
|
|
31993
34070
|
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
31994
34071
|
const paths = this.getSettablePaths({ global });
|
|
31995
34072
|
if (relativeFilePath === paths.root.relativeFilePath) {
|
|
@@ -32056,7 +34133,7 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
|
|
|
32056
34133
|
/**
|
|
32057
34134
|
* Rule generator for Pi Coding Agent.
|
|
32058
34135
|
*
|
|
32059
|
-
* Pi loads instruction context
|
|
34136
|
+
* Pi loads instruction context from the `AGENTS.md` / `CLAUDE.md` family —
|
|
32060
34137
|
* the global `~/.pi/agent/AGENTS.md` plus files discovered by walking up the
|
|
32061
34138
|
* directory tree from the current working directory. It does NOT resolve
|
|
32062
34139
|
* `@`-imports or a TOON file list, and has no `.agents/memories/` concept, so
|
|
@@ -32067,6 +34144,15 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
|
|
|
32067
34144
|
* to map onto; their bodies are folded into the single root `AGENTS.md` by the
|
|
32068
34145
|
* RulesProcessor (there is no separate non-root output location — `nonRoot` is
|
|
32069
34146
|
* `undefined`). This mirrors the codexcli, grokcli, warp, and deepagents targets.
|
|
34147
|
+
*
|
|
34148
|
+
* Pi also loads two system-prompt instruction files that rulesync does NOT emit:
|
|
34149
|
+
* `.pi/SYSTEM.md` (global `~/.pi/agent/SYSTEM.md`) *replaces* the default system
|
|
34150
|
+
* prompt entirely, and `.pi/APPEND_SYSTEM.md` (global
|
|
34151
|
+
* `~/.pi/agent/APPEND_SYSTEM.md`) *appends* to it. rulesync's rules model only
|
|
34152
|
+
* routes a designated `root` rule to a single context file and has no convention
|
|
34153
|
+
* for marking a rule as "replace" vs "append" the system prompt, so these
|
|
34154
|
+
* surfaces are documented in docs/reference/file-formats.md and left to be
|
|
34155
|
+
* authored by hand rather than mapped to a speculative new frontmatter flag.
|
|
32070
34156
|
*/
|
|
32071
34157
|
var PiRule = class PiRule extends ToolRule {
|
|
32072
34158
|
constructor({ fileContent, root, ...rest }) {
|
|
@@ -32657,6 +34743,37 @@ var RovodevRule = class RovodevRule extends ToolRule {
|
|
|
32657
34743
|
toolTarget: "rovodev"
|
|
32658
34744
|
});
|
|
32659
34745
|
}
|
|
34746
|
+
/**
|
|
34747
|
+
* Mirror the primary `.rovodev/AGENTS.md` root rule to `./AGENTS.md` so project
|
|
34748
|
+
* memory stays discoverable at the repo root. Empty when `rootRule` is not the primary root.
|
|
34749
|
+
*/
|
|
34750
|
+
static getRootMirrorFiles({ outputRoot, rootRule, content }) {
|
|
34751
|
+
if (!(rootRule instanceof RovodevRule)) return [];
|
|
34752
|
+
const primary = this.getSettablePaths({ global: false }).root;
|
|
34753
|
+
if (rootRule.getRelativeDirPath() !== primary.relativeDirPath || rootRule.getRelativeFilePath() !== primary.relativeFilePath) return [];
|
|
34754
|
+
return [new RovodevRule({
|
|
34755
|
+
outputRoot,
|
|
34756
|
+
relativeDirPath: ".",
|
|
34757
|
+
relativeFilePath: ROVODEV_RULE_FILE_NAME,
|
|
34758
|
+
fileContent: content,
|
|
34759
|
+
validate: true,
|
|
34760
|
+
root: true
|
|
34761
|
+
})];
|
|
34762
|
+
}
|
|
34763
|
+
/**
|
|
34764
|
+
* Globs for mirror deletion: the `./AGENTS.md` mirror (`mirrorGlob`) is deleted
|
|
34765
|
+
* only when the primary `.rovodev/AGENTS.md` (`primaryGlob`) still exists.
|
|
34766
|
+
*/
|
|
34767
|
+
static getRootMirrorDeletionGlobs({ outputRoot }) {
|
|
34768
|
+
return {
|
|
34769
|
+
primaryGlob: (0, node_path.join)(outputRoot, ROVODEV_DIR, ROVODEV_RULE_FILE_NAME),
|
|
34770
|
+
mirrorGlob: (0, node_path.join)(outputRoot, ROVODEV_RULE_FILE_NAME)
|
|
34771
|
+
};
|
|
34772
|
+
}
|
|
34773
|
+
/** Glob for the `separate-local-file` deletion; rovodev writes it at project root, not under `.rovodev/`. */
|
|
34774
|
+
static getLocalRootDeletionGlob({ outputRoot, fileName }) {
|
|
34775
|
+
return (0, node_path.join)(outputRoot, fileName);
|
|
34776
|
+
}
|
|
32660
34777
|
};
|
|
32661
34778
|
//#endregion
|
|
32662
34779
|
//#region src/features/rules/takt-rule.ts
|
|
@@ -33379,7 +35496,7 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
33379
35496
|
});
|
|
33380
35497
|
this.applyRootRuleSections({
|
|
33381
35498
|
toolRules,
|
|
33382
|
-
|
|
35499
|
+
factory
|
|
33383
35500
|
});
|
|
33384
35501
|
return [...toolRules, ...extraFiles];
|
|
33385
35502
|
}
|
|
@@ -33441,31 +35558,16 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
33441
35558
|
* reference and conventions sections to the root rule content. Mutates the
|
|
33442
35559
|
* root rule in place.
|
|
33443
35560
|
*/
|
|
33444
|
-
applyRootRuleSections({ toolRules,
|
|
35561
|
+
applyRootRuleSections({ toolRules, factory }) {
|
|
35562
|
+
const { meta } = factory;
|
|
33445
35563
|
const rootRule = toolRules.find((rule) => rule.isRoot());
|
|
33446
35564
|
if (!rootRule) return;
|
|
33447
35565
|
const newContent = this.generateReferenceSectionFromMeta(meta, toolRules) + (!meta.createsSeparateConventionsRule && meta.additionalConventions ? this.generateAdditionalConventionsSectionFromMeta(meta) : "") + rootRule.getFileContent();
|
|
33448
35566
|
rootRule.setFileContent(newContent);
|
|
33449
|
-
if (meta.mirrorsRootToAgentsMd && !this.global)
|
|
33450
|
-
|
|
35567
|
+
if (meta.mirrorsRootToAgentsMd && !this.global && factory.class.getRootMirrorFiles) toolRules.push(...factory.class.getRootMirrorFiles({
|
|
35568
|
+
outputRoot: this.outputRoot,
|
|
33451
35569
|
rootRule,
|
|
33452
35570
|
content: newContent
|
|
33453
|
-
});
|
|
33454
|
-
}
|
|
33455
|
-
/**
|
|
33456
|
-
* Mirror the primary root rule to a project-root `AGENTS.md` for tools whose
|
|
33457
|
-
* primary root lives in a subdirectory (rovodev: `.rovodev/AGENTS.md`).
|
|
33458
|
-
*/
|
|
33459
|
-
mirrorRootRuleToAgentsMd({ toolRules, rootRule, content }) {
|
|
33460
|
-
if (!(rootRule instanceof RovodevRule)) return;
|
|
33461
|
-
const primary = RovodevRule.getSettablePaths({ global: false }).root;
|
|
33462
|
-
if (rootRule.getRelativeDirPath() === primary.relativeDirPath && rootRule.getRelativeFilePath() === primary.relativeFilePath) toolRules.push(new RovodevRule({
|
|
33463
|
-
outputRoot: this.outputRoot,
|
|
33464
|
-
relativeDirPath: ".",
|
|
33465
|
-
relativeFilePath: "AGENTS.md",
|
|
33466
|
-
fileContent: content,
|
|
33467
|
-
validate: true,
|
|
33468
|
-
root: true
|
|
33469
35571
|
}));
|
|
33470
35572
|
}
|
|
33471
35573
|
buildSkillList(skillClass) {
|
|
@@ -33691,15 +35793,19 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
33691
35793
|
const localRootToolRules = await (async () => {
|
|
33692
35794
|
if (!forDeletion || this.global || factory.meta.localRootMode !== "separate-local-file" || !factory.meta.localRootFileName) return [];
|
|
33693
35795
|
const fileName = factory.meta.localRootFileName;
|
|
33694
|
-
if (factory.class
|
|
35796
|
+
if (factory.class.getLocalRootDeletionGlob) return buildDeletionRulesFromPaths(await findFilesByGlobs(factory.class.getLocalRootDeletionGlob({
|
|
35797
|
+
outputRoot: this.outputRoot,
|
|
35798
|
+
fileName
|
|
35799
|
+
})));
|
|
33695
35800
|
if (!settablePaths.root) return [];
|
|
33696
35801
|
return buildDeletionRulesFromPaths(await findFilesWithFallback((0, node_path.join)(this.outputRoot, settablePaths.root.relativeDirPath ?? ".", fileName), settablePaths.alternativeRoots, (alt) => (0, node_path.join)(this.outputRoot, alt.relativeDirPath, fileName)));
|
|
33697
35802
|
})();
|
|
33698
35803
|
this.logger.debug(`Found ${localRootToolRules.length} local root tool rule files for deletion`);
|
|
33699
35804
|
const rootMirrorDeletionRules = await (async () => {
|
|
33700
|
-
if (!forDeletion || this.global || !factory.meta.mirrorsRootToAgentsMd || factory.class
|
|
33701
|
-
|
|
33702
|
-
|
|
35805
|
+
if (!forDeletion || this.global || !factory.meta.mirrorsRootToAgentsMd || !factory.class.getRootMirrorDeletionGlobs) return [];
|
|
35806
|
+
const { primaryGlob, mirrorGlob } = factory.class.getRootMirrorDeletionGlobs({ outputRoot: this.outputRoot });
|
|
35807
|
+
if ((await findFilesByGlobs(primaryGlob)).length === 0) return [];
|
|
35808
|
+
return buildDeletionRulesFromPaths(await findFilesByGlobs(mirrorGlob));
|
|
33703
35809
|
})();
|
|
33704
35810
|
const nonRootToolRules = await (async () => {
|
|
33705
35811
|
if (!settablePaths.nonRoot) return [];
|
|
@@ -34148,63 +36254,228 @@ function warnUnsupportedTargets(params) {
|
|
|
34148
36254
|
async function checkRulesyncDirExists(params) {
|
|
34149
36255
|
return fileExists((0, node_path.join)(params.inputRoot, RULESYNC_RELATIVE_DIR_PATH));
|
|
34150
36256
|
}
|
|
36257
|
+
function dependsOnReachable(byId, from, target) {
|
|
36258
|
+
const seen = /* @__PURE__ */ new Set();
|
|
36259
|
+
const stack = [from];
|
|
36260
|
+
while (stack.length > 0) {
|
|
36261
|
+
const current = stack.pop();
|
|
36262
|
+
if (current === void 0 || seen.has(current)) continue;
|
|
36263
|
+
seen.add(current);
|
|
36264
|
+
if (current === target) return true;
|
|
36265
|
+
for (const dep of byId.get(current)?.dependsOn ?? []) stack.push(dep);
|
|
36266
|
+
}
|
|
36267
|
+
return false;
|
|
36268
|
+
}
|
|
36269
|
+
function assertSharedFilesOrdered(steps, byId) {
|
|
36270
|
+
const writersByFile = /* @__PURE__ */ new Map();
|
|
36271
|
+
for (const step of steps) for (const file of step.writesSharedFile ?? []) writersByFile.set(file, [...writersByFile.get(file) ?? [], step.id]);
|
|
36272
|
+
for (const [file, writers] of writersByFile) for (let i = 0; i < writers.length; i++) for (let j = i + 1; j < writers.length; j++) {
|
|
36273
|
+
const a = writers[i];
|
|
36274
|
+
const b = writers[j];
|
|
36275
|
+
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.`);
|
|
36276
|
+
}
|
|
36277
|
+
}
|
|
36278
|
+
/**
|
|
36279
|
+
* Topologically sort generation steps and reject ordering hazards: a shared file
|
|
36280
|
+
* with two writers not ordered by `dependsOn` (a silent data-loss trap), an
|
|
36281
|
+
* unknown dependency, or a cycle. Reordering `steps` stays safe as a result.
|
|
36282
|
+
*
|
|
36283
|
+
* @throws Error if a shared file has unordered writers, a dependency is unknown,
|
|
36284
|
+
* or the dependency graph contains a cycle.
|
|
36285
|
+
*/
|
|
36286
|
+
function resolveExecutionOrder(steps) {
|
|
36287
|
+
const byId = new Map(steps.map((step) => [step.id, step]));
|
|
36288
|
+
assertSharedFilesOrdered(steps, byId);
|
|
36289
|
+
const unresolvedDeps = new Map(steps.map((step) => [step.id, 0]));
|
|
36290
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
36291
|
+
for (const step of steps) for (const dep of step.dependsOn ?? []) {
|
|
36292
|
+
if (!byId.has(dep)) throw new Error(`Generation step '${step.id}' depends on unknown step '${dep}'.`);
|
|
36293
|
+
unresolvedDeps.set(step.id, (unresolvedDeps.get(step.id) ?? 0) + 1);
|
|
36294
|
+
dependents.set(dep, [...dependents.get(dep) ?? [], step.id]);
|
|
36295
|
+
}
|
|
36296
|
+
const ready = steps.filter((step) => (unresolvedDeps.get(step.id) ?? 0) === 0).map((step) => step.id);
|
|
36297
|
+
const ordered = [];
|
|
36298
|
+
while (ready.length > 0) {
|
|
36299
|
+
const id = ready.shift();
|
|
36300
|
+
ordered.push(byId.get(id));
|
|
36301
|
+
for (const dependent of dependents.get(id) ?? []) {
|
|
36302
|
+
const next = (unresolvedDeps.get(dependent) ?? 0) - 1;
|
|
36303
|
+
unresolvedDeps.set(dependent, next);
|
|
36304
|
+
if (next === 0) ready.push(dependent);
|
|
36305
|
+
}
|
|
36306
|
+
}
|
|
36307
|
+
if (ordered.length !== steps.length) throw new Error("Generation steps contain a cyclic 'dependsOn' dependency.");
|
|
36308
|
+
return ordered;
|
|
36309
|
+
}
|
|
36310
|
+
/**
|
|
36311
|
+
* The static shape of the generation step graph: which steps write which shared
|
|
36312
|
+
* (read-modify-write) config files, and the `dependsOn` edges that fix a safe order
|
|
36313
|
+
* for those writers. Exported (separately from the `run` closures, which need a
|
|
36314
|
+
* live `config`/`logger`) so `resolveExecutionOrder`'s ordering guarantee can be
|
|
36315
|
+
* tested directly against the real graph rather than a hand-copied one. Readonly
|
|
36316
|
+
* so a consumer can't mutate this module-level singleton and affect every
|
|
36317
|
+
* subsequent `generate()` call in the process.
|
|
36318
|
+
*/
|
|
36319
|
+
const GENERATION_STEP_GRAPH = [
|
|
36320
|
+
{
|
|
36321
|
+
id: "ignore",
|
|
36322
|
+
writesSharedFile: [".claude/settings.json", ".zed/settings.json"]
|
|
36323
|
+
},
|
|
36324
|
+
{
|
|
36325
|
+
id: "mcp",
|
|
36326
|
+
writesSharedFile: [
|
|
36327
|
+
".amp/settings.json",
|
|
36328
|
+
".augment/settings.json",
|
|
36329
|
+
".codex/config.toml",
|
|
36330
|
+
".config/amp/settings.json",
|
|
36331
|
+
".config/devin/config.json",
|
|
36332
|
+
".config/opencode/opencode.json",
|
|
36333
|
+
".config/zed/settings.json",
|
|
36334
|
+
".devin/config.json",
|
|
36335
|
+
".grok/config.toml",
|
|
36336
|
+
".hermes/config.yaml",
|
|
36337
|
+
".qwen/settings.json",
|
|
36338
|
+
".reasonix/config.toml",
|
|
36339
|
+
".takt/config.yaml",
|
|
36340
|
+
".vibe/config.toml",
|
|
36341
|
+
".zed/settings.json",
|
|
36342
|
+
"kilo.json",
|
|
36343
|
+
"opencode.json",
|
|
36344
|
+
"reasonix.toml"
|
|
36345
|
+
],
|
|
36346
|
+
dependsOn: ["ignore"]
|
|
36347
|
+
},
|
|
36348
|
+
{ id: "commands" },
|
|
36349
|
+
{ id: "subagents" },
|
|
36350
|
+
{ id: "skills" },
|
|
36351
|
+
{
|
|
36352
|
+
id: "hooks",
|
|
36353
|
+
writesSharedFile: [
|
|
36354
|
+
".augment/settings.json",
|
|
36355
|
+
".claude/settings.json",
|
|
36356
|
+
".codex/config.toml",
|
|
36357
|
+
".config/devin/config.json",
|
|
36358
|
+
".hermes/config.yaml",
|
|
36359
|
+
".kiro/agents/default.json",
|
|
36360
|
+
".qwen/settings.json",
|
|
36361
|
+
".vibe/config.toml"
|
|
36362
|
+
],
|
|
36363
|
+
dependsOn: ["ignore", "mcp"]
|
|
36364
|
+
},
|
|
36365
|
+
{
|
|
36366
|
+
id: "permissions",
|
|
36367
|
+
writesSharedFile: [
|
|
36368
|
+
".amp/settings.json",
|
|
36369
|
+
".augment/settings.json",
|
|
36370
|
+
".claude/settings.json",
|
|
36371
|
+
".codex/config.toml",
|
|
36372
|
+
".config/amp/settings.json",
|
|
36373
|
+
".config/devin/config.json",
|
|
36374
|
+
".config/opencode/opencode.json",
|
|
36375
|
+
".config/zed/settings.json",
|
|
36376
|
+
".devin/config.json",
|
|
36377
|
+
".grok/config.toml",
|
|
36378
|
+
".hermes/config.yaml",
|
|
36379
|
+
".kiro/agents/default.json",
|
|
36380
|
+
".qwen/settings.json",
|
|
36381
|
+
".reasonix/config.toml",
|
|
36382
|
+
".takt/config.yaml",
|
|
36383
|
+
".vibe/config.toml",
|
|
36384
|
+
".zed/settings.json",
|
|
36385
|
+
"opencode.json",
|
|
36386
|
+
"reasonix.toml"
|
|
36387
|
+
],
|
|
36388
|
+
dependsOn: [
|
|
36389
|
+
"ignore",
|
|
36390
|
+
"hooks",
|
|
36391
|
+
"mcp"
|
|
36392
|
+
]
|
|
36393
|
+
},
|
|
36394
|
+
{
|
|
36395
|
+
id: "rules",
|
|
36396
|
+
writesSharedFile: ["kilo.json", "opencode.json"],
|
|
36397
|
+
dependsOn: [
|
|
36398
|
+
"mcp",
|
|
36399
|
+
"skills",
|
|
36400
|
+
"permissions"
|
|
36401
|
+
]
|
|
36402
|
+
}
|
|
36403
|
+
];
|
|
34151
36404
|
/**
|
|
34152
36405
|
* Generate configuration files for AI tools.
|
|
34153
36406
|
* @throws Error if generation fails
|
|
34154
36407
|
*/
|
|
34155
36408
|
async function generate(params) {
|
|
34156
36409
|
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
|
-
|
|
36410
|
+
let skillsResult;
|
|
36411
|
+
const runners = {
|
|
36412
|
+
ignore: () => generateIgnoreCore({
|
|
36413
|
+
config,
|
|
36414
|
+
logger
|
|
36415
|
+
}),
|
|
36416
|
+
mcp: () => generateMcpCore({
|
|
36417
|
+
config,
|
|
36418
|
+
logger
|
|
36419
|
+
}),
|
|
36420
|
+
commands: () => generateCommandsCore({
|
|
36421
|
+
config,
|
|
36422
|
+
logger
|
|
36423
|
+
}),
|
|
36424
|
+
subagents: () => generateSubagentsCore({
|
|
36425
|
+
config,
|
|
36426
|
+
logger
|
|
36427
|
+
}),
|
|
36428
|
+
skills: async () => {
|
|
36429
|
+
skillsResult = await generateSkillsCore({
|
|
36430
|
+
config,
|
|
36431
|
+
logger
|
|
36432
|
+
});
|
|
36433
|
+
return skillsResult;
|
|
36434
|
+
},
|
|
36435
|
+
hooks: () => generateHooksCore({
|
|
36436
|
+
config,
|
|
36437
|
+
logger
|
|
36438
|
+
}),
|
|
36439
|
+
permissions: () => generatePermissionsCore({
|
|
36440
|
+
config,
|
|
36441
|
+
logger
|
|
36442
|
+
}),
|
|
36443
|
+
rules: () => generateRulesCore({
|
|
36444
|
+
config,
|
|
36445
|
+
logger,
|
|
36446
|
+
skills: skillsResult?.skills
|
|
36447
|
+
})
|
|
36448
|
+
};
|
|
36449
|
+
const orderedSteps = resolveExecutionOrder(GENERATION_STEP_GRAPH.map((meta) => ({
|
|
36450
|
+
...meta,
|
|
36451
|
+
run: runners[meta.id]
|
|
36452
|
+
})));
|
|
36453
|
+
const resultsById = /* @__PURE__ */ new Map();
|
|
36454
|
+
for (const step of orderedSteps) resultsById.set(step.id, await step.run());
|
|
36455
|
+
if (!skillsResult) throw new Error("Skills generation step did not run.");
|
|
36456
|
+
const get = (id) => {
|
|
36457
|
+
const result = resultsById.get(id);
|
|
36458
|
+
if (!result) throw new Error(`Missing generation result for step '${id}'.`);
|
|
36459
|
+
return result;
|
|
36460
|
+
};
|
|
36461
|
+
const hasDiff = orderedSteps.some((step) => get(step.id).hasDiff);
|
|
34191
36462
|
return {
|
|
34192
|
-
rulesCount:
|
|
34193
|
-
rulesPaths:
|
|
34194
|
-
ignoreCount:
|
|
34195
|
-
ignorePaths:
|
|
34196
|
-
mcpCount:
|
|
34197
|
-
mcpPaths:
|
|
34198
|
-
commandsCount:
|
|
34199
|
-
commandsPaths:
|
|
34200
|
-
subagentsCount:
|
|
34201
|
-
subagentsPaths:
|
|
36463
|
+
rulesCount: get("rules").count,
|
|
36464
|
+
rulesPaths: get("rules").paths,
|
|
36465
|
+
ignoreCount: get("ignore").count,
|
|
36466
|
+
ignorePaths: get("ignore").paths,
|
|
36467
|
+
mcpCount: get("mcp").count,
|
|
36468
|
+
mcpPaths: get("mcp").paths,
|
|
36469
|
+
commandsCount: get("commands").count,
|
|
36470
|
+
commandsPaths: get("commands").paths,
|
|
36471
|
+
subagentsCount: get("subagents").count,
|
|
36472
|
+
subagentsPaths: get("subagents").paths,
|
|
34202
36473
|
skillsCount: skillsResult.count,
|
|
34203
36474
|
skillsPaths: skillsResult.paths,
|
|
34204
|
-
hooksCount:
|
|
34205
|
-
hooksPaths:
|
|
34206
|
-
permissionsCount:
|
|
34207
|
-
permissionsPaths:
|
|
36475
|
+
hooksCount: get("hooks").count,
|
|
36476
|
+
hooksPaths: get("hooks").paths,
|
|
36477
|
+
permissionsCount: get("permissions").count,
|
|
36478
|
+
permissionsPaths: get("permissions").paths,
|
|
34208
36479
|
skills: skillsResult.skills,
|
|
34209
36480
|
hasDiff
|
|
34210
36481
|
};
|