rulesync 43.5.0 → 43.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/index.cjs +5 -5
- package/dist/cli/index.js +5 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-C_XK2L62.js → import--Y_A56qx.js} +469 -136
- package/dist/{import-C_XK2L62.js.map → import--Y_A56qx.js.map} +1 -1
- package/dist/{import-CAePeHTT.cjs → import-C-oT_Hdp.cjs} +485 -152
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -382,36 +382,36 @@ function formatError(error) {
|
|
|
382
382
|
return boundErrorMessage(String(error));
|
|
383
383
|
}
|
|
384
384
|
// src/constants/rulesync-paths.ts
|
|
385
|
-
const { join: join$
|
|
385
|
+
const { join: join$385 } = node_path.posix;
|
|
386
386
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
387
387
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
388
388
|
const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
|
|
389
389
|
const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
|
|
390
390
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
391
391
|
const RULES_FEATURE_SUBDIR = "rules";
|
|
392
|
-
const CURATED_RULES_FEATURE_SUBDIR = join$
|
|
392
|
+
const CURATED_RULES_FEATURE_SUBDIR = join$385(RULES_FEATURE_SUBDIR, ".curated");
|
|
393
393
|
const COMMANDS_FEATURE_SUBDIR = "commands";
|
|
394
394
|
const SUBAGENTS_FEATURE_SUBDIR = "subagents";
|
|
395
395
|
const CHECKS_FEATURE_SUBDIR = "checks";
|
|
396
396
|
const SKILLS_FEATURE_SUBDIR = "skills";
|
|
397
|
-
const CURATED_SKILLS_FEATURE_SUBDIR = join$
|
|
398
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
399
|
-
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$
|
|
400
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
401
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
402
|
-
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$
|
|
403
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
404
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
405
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
406
|
-
join$
|
|
407
|
-
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
408
|
-
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
397
|
+
const CURATED_SKILLS_FEATURE_SUBDIR = join$385(SKILLS_FEATURE_SUBDIR, ".curated");
|
|
398
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
|
|
399
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
|
|
400
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
|
|
401
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
|
|
402
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
|
|
403
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
404
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
405
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
406
|
+
join$385(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
407
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
408
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
409
409
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
410
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
410
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
411
411
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
412
412
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
413
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
414
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
413
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
|
|
414
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
415
415
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
416
416
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
417
417
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -895,6 +895,7 @@ const hooksProcessorToolTargetTuple = [
|
|
|
895
895
|
"qwencode",
|
|
896
896
|
"reasonix",
|
|
897
897
|
"tabnine",
|
|
898
|
+
"trae",
|
|
898
899
|
"grokcli",
|
|
899
900
|
"gitlabduo",
|
|
900
901
|
"lettacode",
|
|
@@ -3945,6 +3946,7 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
|
|
|
3945
3946
|
".poolside/settings.yaml",
|
|
3946
3947
|
".config/poolside/settings.yaml",
|
|
3947
3948
|
".tabnine/agent/settings.json",
|
|
3949
|
+
".trae/hooks.json",
|
|
3948
3950
|
".vibe/config.toml",
|
|
3949
3951
|
".zcode/config.json",
|
|
3950
3952
|
".zcode/cli/config.json",
|
|
@@ -6027,6 +6029,34 @@ const CANONICAL_TO_CORTEXCODE_EVENT_NAMES = {
|
|
|
6027
6029
|
};
|
|
6028
6030
|
const CORTEXCODE_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_CORTEXCODE_EVENT_NAMES);
|
|
6029
6031
|
/**
|
|
6032
|
+
* Hook events supported by TRAE.
|
|
6033
|
+
*
|
|
6034
|
+
* TRAE reads `.trae/hooks.json` (project) and `~/.trae/hooks.json` (user):
|
|
6035
|
+
* `{ "version"?: 1, "hooks": { "<Event>": [{ "matcher"?, "hooks": [{ "type":
|
|
6036
|
+
* "command", "command", "timeout"? }] }] } }` with Claude-Code-style
|
|
6037
|
+
* PascalCase event names. `$TRAE_PROJECT_DIR` resolves to the workspace root
|
|
6038
|
+
* inside commands.
|
|
6039
|
+
*
|
|
6040
|
+
* @see https://docs.trae.ai/ide/hook-configuration-reference?_lang=en
|
|
6041
|
+
*/
|
|
6042
|
+
const TRAE_HOOK_EVENTS = [
|
|
6043
|
+
"sessionStart",
|
|
6044
|
+
"beforeSubmitPrompt",
|
|
6045
|
+
"preToolUse",
|
|
6046
|
+
"postToolUse",
|
|
6047
|
+
"stop",
|
|
6048
|
+
"notification"
|
|
6049
|
+
];
|
|
6050
|
+
const CANONICAL_TO_TRAE_EVENT_NAMES = {
|
|
6051
|
+
sessionStart: "SessionStart",
|
|
6052
|
+
beforeSubmitPrompt: "UserPromptSubmit",
|
|
6053
|
+
preToolUse: "PreToolUse",
|
|
6054
|
+
postToolUse: "PostToolUse",
|
|
6055
|
+
stop: "Stop",
|
|
6056
|
+
notification: "Notification"
|
|
6057
|
+
};
|
|
6058
|
+
const TRAE_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_TRAE_EVENT_NAMES);
|
|
6059
|
+
/**
|
|
6030
6060
|
* Hook events supported by CodeBuddy Code.
|
|
6031
6061
|
*
|
|
6032
6062
|
* CodeBuddy Code reads hooks from the `hooks` key of `.codebuddy/settings.json`
|
|
@@ -6499,6 +6529,7 @@ const HooksConfigSchema = zod_mini.z.looseObject({
|
|
|
6499
6529
|
bob: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6500
6530
|
codebuddy: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6501
6531
|
cortexcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6532
|
+
trae: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6502
6533
|
commandcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6503
6534
|
lettacode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6504
6535
|
continue: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
@@ -14181,6 +14212,14 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
14181
14212
|
}
|
|
14182
14213
|
}
|
|
14183
14214
|
},
|
|
14215
|
+
".trae/hooks.json": {
|
|
14216
|
+
format: "json",
|
|
14217
|
+
invalidRootPolicy: "error",
|
|
14218
|
+
features: { hooks: {
|
|
14219
|
+
kind: "replace-owned-keys",
|
|
14220
|
+
ownedKeys: ["hooks"]
|
|
14221
|
+
} }
|
|
14222
|
+
},
|
|
14184
14223
|
".trae/permission/global.json": {
|
|
14185
14224
|
format: "json",
|
|
14186
14225
|
invalidRootPolicy: "error",
|
|
@@ -23306,10 +23345,13 @@ var TaktCommand = class TaktCommand extends ToolCommand {
|
|
|
23306
23345
|
* @see https://docs.trae.ai/ide/slash-commands?_lang=en
|
|
23307
23346
|
* @see https://docs.trae.ai/ide/ignore-files?_lang=en
|
|
23308
23347
|
* @see https://docs.trae.ai/ide/permission-and-approval?_lang=en
|
|
23348
|
+
* @see https://docs.trae.ai/ide/hook-configuration-reference?_lang=en
|
|
23309
23349
|
*/
|
|
23310
23350
|
const TRAE_DIR = ".trae";
|
|
23311
23351
|
/** Project MCP servers (`mcpServers` object), project scope only. */
|
|
23312
23352
|
const TRAE_MCP_FILE_NAME = "mcp.json";
|
|
23353
|
+
/** Hooks: `.trae/hooks.json` (project) and `~/.trae/hooks.json` (global). */
|
|
23354
|
+
const TRAE_HOOKS_FILE_NAME = "hooks.json";
|
|
23313
23355
|
/** Skills: `.trae/skills/<name>/SKILL.md` (project) and `~/.trae/skills/` (global). */
|
|
23314
23356
|
const TRAE_SKILLS_DIR_PATH = (0, node_path.join)(TRAE_DIR, "skills");
|
|
23315
23357
|
/**
|
|
@@ -25234,12 +25276,80 @@ var AiassistantSkill = class AiassistantSkill extends ToolSkill {
|
|
|
25234
25276
|
});
|
|
25235
25277
|
}
|
|
25236
25278
|
};
|
|
25279
|
+
// src/features/shared/amp-settings.ts
|
|
25280
|
+
/**
|
|
25281
|
+
* Parse an Amp `settings.json` / `settings.jsonc` document.
|
|
25282
|
+
*
|
|
25283
|
+
* Shared by the MCP and permissions adapters because both read the very same
|
|
25284
|
+
* file. Two separately maintained copies drifted once already: the permissions
|
|
25285
|
+
* side was hardened while the MCP side kept calling `jsonc-parser` directly,
|
|
25286
|
+
* which left it importing MCP servers reachable only through an injected
|
|
25287
|
+
* `__proto__`.
|
|
25288
|
+
*
|
|
25289
|
+
* {@link parseJsonc} is the strict, sanitizing parser: it throws on any syntax
|
|
25290
|
+
* error instead of returning `jsonc-parser`'s best-effort value, and it
|
|
25291
|
+
* rebuilds every object from its own enumerable entries, so a root `__proto__`
|
|
25292
|
+
* cannot swap the returned object's prototype and `constructor` / `prototype`
|
|
25293
|
+
* never survive as own keys, at any depth.
|
|
25294
|
+
*
|
|
25295
|
+
* Removal is silent, matching every other tool-side adapter (opencode, kilo,
|
|
25296
|
+
* copilot). Reporting the removed keys through `droppedPollutionKeysError` is
|
|
25297
|
+
* reserved for the three files a user authors under `.rulesync/`, whose whole
|
|
25298
|
+
* purpose is to be turned into tool config — a key that vanishes there needs
|
|
25299
|
+
* explaining, whereas here the surrounding settings are the user's own file
|
|
25300
|
+
* and are left alone.
|
|
25301
|
+
*
|
|
25302
|
+
* Note the consequence for a root `__proto__`: sanitizing runs before the
|
|
25303
|
+
* plain-object check, so such a document parses successfully with the key
|
|
25304
|
+
* stripped rather than failing that check. That is deliberate — dropping the
|
|
25305
|
+
* one poisoned key keeps the user's unrelated settings — and matches the
|
|
25306
|
+
* sanitize-before-the-root-check design the shared config gateway documents.
|
|
25307
|
+
*/
|
|
25308
|
+
function parseAmpSettings({ fileContent }) {
|
|
25309
|
+
let parsed;
|
|
25310
|
+
try {
|
|
25311
|
+
parsed = parseJsonc$6(fileContent || "{}");
|
|
25312
|
+
} catch (error) {
|
|
25313
|
+
throw new Error(`Failed to parse Amp settings: ${formatError(error)}`, { cause: error });
|
|
25314
|
+
}
|
|
25315
|
+
if (!isPlainObject(parsed)) throw new Error("Amp settings must be a JSON object");
|
|
25316
|
+
return parsed;
|
|
25317
|
+
}
|
|
25318
|
+
/**
|
|
25319
|
+
* Probe `<jsonDir>/settings.jsonc` first, falling back to `settings.json`, so
|
|
25320
|
+
* an existing user file is read-modified-written in place instead of a fresh
|
|
25321
|
+
* `.json` sibling being created next to a hand-authored `.jsonc`. Defaults to
|
|
25322
|
+
* `settings.json` when neither file exists: Amp's canonically documented
|
|
25323
|
+
* settings file (`.amp/settings.json`), shared by the MCP and permissions
|
|
25324
|
+
* adapters, so generating both on a clean repo writes a single settings file.
|
|
25325
|
+
*/
|
|
25326
|
+
async function resolveAmpSettingsFile(jsonDir) {
|
|
25327
|
+
const jsoncContent = await readFileContentOrNull((0, node_path.join)(jsonDir, AMP_SETTINGS_JSONC_FILE_NAME));
|
|
25328
|
+
if (jsoncContent !== null) return {
|
|
25329
|
+
fileContent: jsoncContent,
|
|
25330
|
+
relativeFilePath: AMP_SETTINGS_JSONC_FILE_NAME
|
|
25331
|
+
};
|
|
25332
|
+
const jsonContent = await readFileContentOrNull((0, node_path.join)(jsonDir, AMP_SETTINGS_FILE_NAME));
|
|
25333
|
+
if (jsonContent !== null) return {
|
|
25334
|
+
fileContent: jsonContent,
|
|
25335
|
+
relativeFilePath: AMP_SETTINGS_FILE_NAME
|
|
25336
|
+
};
|
|
25337
|
+
return {
|
|
25338
|
+
fileContent: null,
|
|
25339
|
+
relativeFilePath: AMP_SETTINGS_FILE_NAME
|
|
25340
|
+
};
|
|
25341
|
+
}
|
|
25237
25342
|
// src/features/skills/amp-skill.ts
|
|
25238
25343
|
const AmpSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
25239
25344
|
name: zod_mini.z.string(),
|
|
25240
25345
|
description: zod_mini.z.string()
|
|
25241
25346
|
});
|
|
25242
25347
|
/**
|
|
25348
|
+
* Amp setting that makes it skip `~/.config/agents/skills/` and
|
|
25349
|
+
* `~/.agents/skills/`. https://ampcode.com/docs/cli/settings
|
|
25350
|
+
*/
|
|
25351
|
+
const AMP_DISABLE_GLOBAL_AGENTS_SKILLS_KEY = "amp.skills.disableGlobalAgentsSkills";
|
|
25352
|
+
/**
|
|
25243
25353
|
* Represents an Amp Agent Skill directory.
|
|
25244
25354
|
*
|
|
25245
25355
|
* Amp natively reads Agent Skills following the open standard: a directory with a
|
|
@@ -25274,6 +25384,27 @@ var AmpSkill = class AmpSkill extends ToolSkill {
|
|
|
25274
25384
|
...global && { importOnlySkillRoots: [AGENTSMD_SKILLS_DIR_PATH, AMP_SKILLS_NATIVE_GLOBAL_DIR] }
|
|
25275
25385
|
};
|
|
25276
25386
|
}
|
|
25387
|
+
/**
|
|
25388
|
+
* Warn when the user's Amp settings disable the global agent-agnostic skill
|
|
25389
|
+
* roots, because the global skills rulesync writes to
|
|
25390
|
+
* `~/.config/agents/skills/` would then never load. Only the user settings
|
|
25391
|
+
* file is checked: a workspace `.amp/settings.json(c)` depends on where Amp
|
|
25392
|
+
* is started, which global generation cannot know.
|
|
25393
|
+
*/
|
|
25394
|
+
static async getGenerationWarnings({ outputRoot, global, logger }) {
|
|
25395
|
+
if (!global) return [];
|
|
25396
|
+
const { fileContent, relativeFilePath } = await resolveAmpSettingsFile((0, node_path.join)(outputRoot, AMP_GLOBAL_DIR));
|
|
25397
|
+
if (fileContent === null) return [];
|
|
25398
|
+
let settings;
|
|
25399
|
+
try {
|
|
25400
|
+
settings = parseAmpSettings({ fileContent });
|
|
25401
|
+
} catch (error) {
|
|
25402
|
+
logger.debug(`Skipping the Amp skills settings check for ${(0, node_path.join)(AMP_GLOBAL_DIR, relativeFilePath)}: ${formatError(error)}`);
|
|
25403
|
+
return [];
|
|
25404
|
+
}
|
|
25405
|
+
if (settings[AMP_DISABLE_GLOBAL_AGENTS_SKILLS_KEY] !== true) return [];
|
|
25406
|
+
return [`Amp will not load the global skills rulesync writes to ~/${AMP_SKILLS_GLOBAL_DIR}/: '${AMP_DISABLE_GLOBAL_AGENTS_SKILLS_KEY}' is true in ~/${(0, node_path.join)(AMP_GLOBAL_DIR, relativeFilePath)}.`];
|
|
25407
|
+
}
|
|
25277
25408
|
getFrontmatter() {
|
|
25278
25409
|
return AmpSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
25279
25410
|
}
|
|
@@ -33250,7 +33381,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
33250
33381
|
async convertRulesyncDirsToToolDirs(rulesyncDirs) {
|
|
33251
33382
|
const rulesyncSkills = rulesyncDirs.filter((dir) => dir instanceof RulesyncSkill);
|
|
33252
33383
|
const factory = this.getFactory(this.toolTarget);
|
|
33253
|
-
|
|
33384
|
+
const toolSkills = (await Promise.all(rulesyncSkills.map(async (rulesyncSkill) => {
|
|
33254
33385
|
const isClaudecodeScheduledTask = rulesyncSkill.getFrontmatter().claudecode?.["scheduled-task"] === true;
|
|
33255
33386
|
if (isClaudecodeScheduledTask && this.toolTarget !== "claudecode" && this.toolTarget !== "claudecode-legacy") return null;
|
|
33256
33387
|
if (!factory.class.isTargetedByRulesyncSkill(rulesyncSkill)) return null;
|
|
@@ -33277,6 +33408,15 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
33277
33408
|
logger: this.logger
|
|
33278
33409
|
});
|
|
33279
33410
|
}))).filter((skill) => skill !== null);
|
|
33411
|
+
if (toolSkills.length > 0 && factory.class.getGenerationWarnings) {
|
|
33412
|
+
const warnings = await factory.class.getGenerationWarnings({
|
|
33413
|
+
outputRoot: this.outputRoot,
|
|
33414
|
+
global: this.global,
|
|
33415
|
+
logger: this.logger
|
|
33416
|
+
});
|
|
33417
|
+
for (const warning of warnings) this.logger.warn(warning);
|
|
33418
|
+
}
|
|
33419
|
+
return toolSkills;
|
|
33280
33420
|
}
|
|
33281
33421
|
async convertToolDirsToRulesyncDirs(toolDirs) {
|
|
33282
33422
|
const toolSkills = toolDirs.filter((dir) => dir instanceof ToolSkill);
|
|
@@ -47482,6 +47622,45 @@ var DshRule = class DshRule extends NestedAgentsmdRule {
|
|
|
47482
47622
|
};
|
|
47483
47623
|
}
|
|
47484
47624
|
};
|
|
47625
|
+
// src/features/rules/output-style-paths.ts
|
|
47626
|
+
/**
|
|
47627
|
+
* Shared helpers for tool rule adapters that write opted-in rules as custom
|
|
47628
|
+
* output styles (`<tool dir>/output-styles/<name>.md`), e.g. Qoder and
|
|
47629
|
+
* Factory Droid. Both tools read only the direct `.md` children of their
|
|
47630
|
+
* output-styles directory, with optional `name` / `description` frontmatter.
|
|
47631
|
+
*/
|
|
47632
|
+
/**
|
|
47633
|
+
* Whether the given path is a direct `.md` child of `outputStylesDirPath`.
|
|
47634
|
+
*/
|
|
47635
|
+
function isDirectMarkdownChild({ outputStylesDirPath, relativeDirPath, relativeFilePath }) {
|
|
47636
|
+
return relativeDirPath === outputStylesDirPath && (0, node_path.extname)(relativeFilePath) === ".md" && (0, node_path.basename)(relativeFilePath) === relativeFilePath;
|
|
47637
|
+
}
|
|
47638
|
+
/**
|
|
47639
|
+
* Whether a rule opts into the `output-style` channel: only a non-root rule
|
|
47640
|
+
* can, since an output style is a file of its own.
|
|
47641
|
+
*/
|
|
47642
|
+
function isOutputStyleChannel({ root, channel }) {
|
|
47643
|
+
return !root && channel === "output-style";
|
|
47644
|
+
}
|
|
47645
|
+
/**
|
|
47646
|
+
* Return the file stem of an output-style source rule, throwing `message`
|
|
47647
|
+
* when the rule is nested in a subdirectory of the rules directory: the
|
|
47648
|
+
* output file is named after the rule, and the tools read only the top level
|
|
47649
|
+
* of their output-styles directory.
|
|
47650
|
+
*/
|
|
47651
|
+
function resolveTopLevelOutputStyleStem({ sourcePath, message }) {
|
|
47652
|
+
if ((0, node_path.basename)(sourcePath) !== sourcePath) throw new Error(message);
|
|
47653
|
+
return (0, node_path.basename)(sourcePath, (0, node_path.extname)(sourcePath));
|
|
47654
|
+
}
|
|
47655
|
+
/**
|
|
47656
|
+
* The output style's frontmatter: only the `name` and `description` that are set.
|
|
47657
|
+
*/
|
|
47658
|
+
function buildOutputStyleFrontmatter({ name, description }) {
|
|
47659
|
+
return {
|
|
47660
|
+
...name !== void 0 && { name },
|
|
47661
|
+
...description !== void 0 && { description }
|
|
47662
|
+
};
|
|
47663
|
+
}
|
|
47485
47664
|
// src/features/rules/factorydroid-rule.ts
|
|
47486
47665
|
/**
|
|
47487
47666
|
* The file-name glob `getExtraFixedFiles` returns for the output-styles
|
|
@@ -47586,7 +47765,11 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
47586
47765
|
* child of the output-styles directory, at either scope.
|
|
47587
47766
|
*/
|
|
47588
47767
|
static isOutputStylePath({ relativeDirPath, relativeFilePath }) {
|
|
47589
|
-
return
|
|
47768
|
+
return isDirectMarkdownChild({
|
|
47769
|
+
outputStylesDirPath: this.getOutputStylesDirPath(),
|
|
47770
|
+
relativeDirPath,
|
|
47771
|
+
relativeFilePath
|
|
47772
|
+
});
|
|
47590
47773
|
}
|
|
47591
47774
|
/**
|
|
47592
47775
|
* Which fixed-file channel, if any, owns the given output path. Matching on
|
|
@@ -47626,7 +47809,10 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
47626
47809
|
/** Whether a rule opts into the `output-style` channel (non-root only). */
|
|
47627
47810
|
static isOutputStyleRule(rulesyncRule) {
|
|
47628
47811
|
const frontmatter = rulesyncRule.getFrontmatter();
|
|
47629
|
-
return
|
|
47812
|
+
return isOutputStyleChannel({
|
|
47813
|
+
root: frontmatter.root,
|
|
47814
|
+
channel: frontmatter.factorydroid?.channel
|
|
47815
|
+
});
|
|
47630
47816
|
}
|
|
47631
47817
|
/**
|
|
47632
47818
|
* Factory Droid loads the channel files itself, so listing one in the root
|
|
@@ -47758,15 +47944,17 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
47758
47944
|
static buildOutputStyle({ outputRoot, rulesyncRule, validate }) {
|
|
47759
47945
|
const frontmatter = rulesyncRule.getFrontmatter();
|
|
47760
47946
|
const sourcePath = rulesyncRule.getRelativeFilePath();
|
|
47761
|
-
|
|
47762
|
-
|
|
47947
|
+
const stem = resolveTopLevelOutputStyleStem({
|
|
47948
|
+
sourcePath,
|
|
47949
|
+
message: `Rule '${sourcePath}' sets factorydroid.channel: output-style but is nested in a subdirectory: Factory Droid loads only direct .md children of .factory/output-styles/, so move it to the top of ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`
|
|
47950
|
+
});
|
|
47763
47951
|
const name = frontmatter.factorydroid?.name;
|
|
47764
47952
|
const styleName = name ?? stem;
|
|
47765
47953
|
if (FACTORYDROID_RESERVED_OUTPUT_STYLE_NAMES.includes(styleName.trim().toLowerCase())) throw new Error(`Rule '${sourcePath}' sets factorydroid.channel: output-style with the style name '${styleName}', which is reserved: Factory Droid's built-in Default and Concise styles cannot be replaced. Rename the rule file or set factorydroid.name.`);
|
|
47766
|
-
const styleFrontmatter = {
|
|
47767
|
-
|
|
47768
|
-
|
|
47769
|
-
};
|
|
47954
|
+
const styleFrontmatter = buildOutputStyleFrontmatter({
|
|
47955
|
+
name,
|
|
47956
|
+
description: frontmatter.description
|
|
47957
|
+
});
|
|
47770
47958
|
const body = rulesyncRule.getBody();
|
|
47771
47959
|
return new FactorydroidRule({
|
|
47772
47960
|
outputRoot,
|
|
@@ -49402,12 +49590,19 @@ var QoderRule = class QoderRule extends ToolRule {
|
|
|
49402
49590
|
* `.qoder/output-styles/`, at either scope.
|
|
49403
49591
|
*/
|
|
49404
49592
|
static isOutputStylePath({ relativeDirPath, relativeFilePath }) {
|
|
49405
|
-
return
|
|
49593
|
+
return isDirectMarkdownChild({
|
|
49594
|
+
outputStylesDirPath: QODER_OUTPUT_STYLES_DIR_PATH,
|
|
49595
|
+
relativeDirPath,
|
|
49596
|
+
relativeFilePath
|
|
49597
|
+
});
|
|
49406
49598
|
}
|
|
49407
49599
|
/** Whether a rule opts into the `output-style` channel (non-root only). */
|
|
49408
49600
|
static isOutputStyleRule(rulesyncRule) {
|
|
49409
49601
|
const frontmatter = rulesyncRule.getFrontmatter();
|
|
49410
|
-
return
|
|
49602
|
+
return isOutputStyleChannel({
|
|
49603
|
+
root: frontmatter.root,
|
|
49604
|
+
channel: frontmatter.qoder?.channel
|
|
49605
|
+
});
|
|
49411
49606
|
}
|
|
49412
49607
|
/**
|
|
49413
49608
|
* The output-styles directory, enumerated for import only: it also holds
|
|
@@ -49518,16 +49713,18 @@ var QoderRule = class QoderRule extends ToolRule {
|
|
|
49518
49713
|
static buildOutputStyle({ outputRoot, rulesyncRule, validate }) {
|
|
49519
49714
|
const { description, qoder } = rulesyncRule.getFrontmatter();
|
|
49520
49715
|
const sourcePath = rulesyncRule.getRelativeFilePath();
|
|
49521
|
-
|
|
49522
|
-
|
|
49716
|
+
const stem = resolveTopLevelOutputStyleStem({
|
|
49717
|
+
sourcePath,
|
|
49718
|
+
message: `Rule '${sourcePath}' sets qoder.channel: output-style but is nested in a subdirectory: Qoder reads only the files directly in .qoder/output-styles/, so move it to the top of ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`
|
|
49719
|
+
});
|
|
49523
49720
|
return new QoderRule({
|
|
49524
49721
|
outputRoot,
|
|
49525
49722
|
relativeDirPath: QODER_OUTPUT_STYLES_DIR_PATH,
|
|
49526
|
-
relativeFilePath: `${
|
|
49527
|
-
frontmatter: {
|
|
49528
|
-
|
|
49529
|
-
|
|
49530
|
-
},
|
|
49723
|
+
relativeFilePath: `${stem}.md`,
|
|
49724
|
+
frontmatter: buildOutputStyleFrontmatter({
|
|
49725
|
+
name: qoder?.name,
|
|
49726
|
+
description
|
|
49727
|
+
}),
|
|
49531
49728
|
body: rulesyncRule.getBody(),
|
|
49532
49729
|
validate,
|
|
49533
49730
|
root: false,
|
|
@@ -56205,9 +56402,9 @@ function groupDefinitionsByMatcher({ definitions, eventName, converterConfig, wa
|
|
|
56205
56402
|
});
|
|
56206
56403
|
translatedMatchers.set(normalizedMatcher, matcher);
|
|
56207
56404
|
}
|
|
56208
|
-
const key = [matcher, ...subdividingFields.map(({ canonical, valueType }) => {
|
|
56405
|
+
const key = [matcher, ...subdividingFields.map(({ canonical, valueType, events }) => {
|
|
56209
56406
|
const value = def[canonical];
|
|
56210
|
-
return isGroupPassthroughValue(value, valueType) ? stableJson(value) : "";
|
|
56407
|
+
return isGroupFieldOnEvent(events, eventName) && isGroupPassthroughValue(value, valueType) ? stableJson(value) : "";
|
|
56211
56408
|
})].join("\0");
|
|
56212
56409
|
const group = byMatcher.get(key);
|
|
56213
56410
|
if (group) group.defs.push(def);
|
|
@@ -56248,6 +56445,7 @@ function runsInPosixShell({ shell, converterConfig }) {
|
|
|
56248
56445
|
* Apply the optional project directory variable prefix to a command string.
|
|
56249
56446
|
*/
|
|
56250
56447
|
function applyCommandPrefix({ def, converterConfig }) {
|
|
56448
|
+
if (converterConfig.stripsProjectDirVarOnImportOnly) return def.command;
|
|
56251
56449
|
const commandText = def.command;
|
|
56252
56450
|
const trimmedCommand = typeof commandText === "string" ? commandText.trimStart() : void 0;
|
|
56253
56451
|
const unquotedCommand = trimmedCommand?.replace(/^["']/, "");
|
|
@@ -56456,12 +56654,16 @@ const describeInvalidObject = ({ tool, canonical, value }) => isPlainObject(valu
|
|
|
56456
56654
|
value
|
|
56457
56655
|
})} Importing it would fail validation on the next run.` : `Dropping "${tool}" while importing a hook: it must be an object.`;
|
|
56458
56656
|
const describeInvalidRecord = ({ tool }) => `Dropping "${tool}" while importing a hook: it must be a map of strings whose keys are non-empty and free of "=", and whose keys and values carry no newline, carriage return or NUL characters.`;
|
|
56657
|
+
function isGroupFieldOnEvent(events, eventName) {
|
|
56658
|
+
return events === void 0 || events.has(eventName);
|
|
56659
|
+
}
|
|
56459
56660
|
/**
|
|
56460
56661
|
* Check a value against the shape its field documents. A string field also
|
|
56461
56662
|
* rejects control characters, matching the canonical `safeString` so an
|
|
56462
56663
|
* imported value cannot fail validation on the next generate.
|
|
56463
56664
|
*/
|
|
56464
56665
|
function isGroupPassthroughValue(value, valueType = "object") {
|
|
56666
|
+
if (valueType === "number") return typeof value === "number" && Number.isFinite(value);
|
|
56465
56667
|
if (valueType === "string") return typeof value === "string" && !CONTROL_CHARS.some((char) => value.includes(char));
|
|
56466
56668
|
return isPlainObject(value);
|
|
56467
56669
|
}
|
|
@@ -56471,10 +56673,14 @@ function isGroupPassthroughValue(value, valueType = "object") {
|
|
|
56471
56673
|
*/
|
|
56472
56674
|
function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }) {
|
|
56473
56675
|
const emitted = {};
|
|
56474
|
-
for (const { canonical, tool, valueType } of converterConfig.groupPassthroughFields ?? []) {
|
|
56676
|
+
for (const { canonical, tool, valueType, events } of converterConfig.groupPassthroughFields ?? []) {
|
|
56475
56677
|
const carried = defs.map((def) => def[canonical]);
|
|
56476
56678
|
const first = carried.find((value) => isGroupPassthroughValue(value, valueType));
|
|
56477
56679
|
if (first === void 0) continue;
|
|
56680
|
+
if (!isGroupFieldOnEvent(events, eventName)) {
|
|
56681
|
+
logger?.warn(`"${canonical}" on "${eventName}" hooks is ignored — the tool supports "${tool}" only on ${[...events ?? []].map((event) => `"${event}"`).join(", ")} hooks.`);
|
|
56682
|
+
continue;
|
|
56683
|
+
}
|
|
56478
56684
|
const firstStable = stableJson(first);
|
|
56479
56685
|
const agrees = (value) => isGroupPassthroughValue(value, valueType) && stableJson(value) === firstStable;
|
|
56480
56686
|
if (!carried.every(agrees)) logger?.warn(`"${tool}" belongs to the whole matcher group on "${eventName}" hooks, so every hook in this group gets ${quoteValueForWarning(first)} — including any that asked for something else, or for nothing.`);
|
|
@@ -56486,9 +56692,9 @@ function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }
|
|
|
56486
56692
|
* Import the configured group-level passthrough fields onto a definition,
|
|
56487
56693
|
* reversing {@link emitGroupPassthroughFields}.
|
|
56488
56694
|
*/
|
|
56489
|
-
function importGroupPassthroughFields({ rawEntry, converterConfig }) {
|
|
56695
|
+
function importGroupPassthroughFields({ rawEntry, eventName, converterConfig }) {
|
|
56490
56696
|
const entry = rawEntry;
|
|
56491
|
-
return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool, valueType }) => isGroupPassthroughValue(entry[tool], valueType)).map(({ canonical, tool }) => [canonical, entry[tool]]));
|
|
56697
|
+
return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool, valueType, events }) => isGroupFieldOnEvent(events, eventName) && isGroupPassthroughValue(entry[tool], valueType)).map(({ canonical, tool }) => [canonical, entry[tool]]));
|
|
56492
56698
|
}
|
|
56493
56699
|
/**
|
|
56494
56700
|
* Emit the payload fields specific to a hook type — `url`/`headers`/
|
|
@@ -56814,19 +57020,23 @@ function stripCommandPrefix({ command, args, shell, converterConfig, warn }) {
|
|
|
56814
57020
|
const cmd = typeof command === "string" ? command : void 0;
|
|
56815
57021
|
const { projectDirVar } = converterConfig;
|
|
56816
57022
|
if (projectDirVar === "" || typeof cmd !== "string") return cmd;
|
|
56817
|
-
|
|
56818
|
-
|
|
56819
|
-
|
|
56820
|
-
|
|
56821
|
-
|
|
56822
|
-
|
|
56823
|
-
|
|
56824
|
-
|
|
56825
|
-
|
|
56826
|
-
|
|
56827
|
-
|
|
56828
|
-
|
|
56829
|
-
|
|
57023
|
+
let stripped = cmd;
|
|
57024
|
+
for (const variable of [projectDirVar, ...converterConfig.importedProjectDirVarAliases ?? []]) {
|
|
57025
|
+
const result = importProjectDirVariable({
|
|
57026
|
+
command: stripped,
|
|
57027
|
+
projectDirVar: variable,
|
|
57028
|
+
firstWordOnly: isExecFormHook({
|
|
57029
|
+
args,
|
|
57030
|
+
converterConfig
|
|
57031
|
+
}) || !runsInPosixShell({
|
|
57032
|
+
shell,
|
|
57033
|
+
converterConfig
|
|
57034
|
+
})
|
|
57035
|
+
});
|
|
57036
|
+
stripped = result.command;
|
|
57037
|
+
if (warn && result.unrestored && !converterConfig.stripsProjectDirVarOnImportOnly) warn(`Hook command ${quoteValueForWarning(cmd)} was imported as ${quoteValueForWarning(stripped)}: a ${variable} path that is not a script the command runs becomes relative to the hook's working directory and is not restored on generate. Put the exact command in a tool-specific hooks override (such as "claudecode.hooks") to keep the variable.`);
|
|
57038
|
+
if (warn && result.retained) warn(`Hook command ${quoteValueForWarning(cmd)} keeps ${variable} in an assignment or option value, or in a later word of a command whose later words are not normalized, so the imported command still refers to it and other tools that do not define it receive it verbatim. Put the exact command in a tool-specific hooks override (such as "claudecode.hooks") if other tools should not see it.`);
|
|
57039
|
+
}
|
|
56830
57040
|
return stripped;
|
|
56831
57041
|
}
|
|
56832
57042
|
/**
|
|
@@ -56867,7 +57077,7 @@ function isStringRecord(value) {
|
|
|
56867
57077
|
}
|
|
56868
57078
|
/** Compare object values without letting key order decide the answer. */
|
|
56869
57079
|
function stableJson(value) {
|
|
56870
|
-
if (typeof value
|
|
57080
|
+
if (typeof value !== "object") return JSON.stringify(value);
|
|
56871
57081
|
return JSON.stringify(Object.fromEntries(Object.entries(value).toSorted(([a], [b]) => a.localeCompare(b))));
|
|
56872
57082
|
}
|
|
56873
57083
|
/**
|
|
@@ -57107,6 +57317,7 @@ function toolHookToCanonical({ h, rawEntry, eventName, converterConfig, warn })
|
|
|
57107
57317
|
}),
|
|
57108
57318
|
...importGroupPassthroughFields({
|
|
57109
57319
|
rawEntry,
|
|
57320
|
+
eventName,
|
|
57110
57321
|
converterConfig
|
|
57111
57322
|
}),
|
|
57112
57323
|
...matcher !== void 0 && matcher !== null && matcher !== "" && { matcher }
|
|
@@ -57159,15 +57370,26 @@ function definingFields({ h, rawEntry, hookType, converterConfig }) {
|
|
|
57159
57370
|
* is the group-level twin of `matcher`: importing the group without it would
|
|
57160
57371
|
* widen every hook in it, so the group is skipped instead.
|
|
57161
57372
|
*/
|
|
57162
|
-
function describeGroupSkipReason({ rawEntry, converterConfig }) {
|
|
57373
|
+
function describeGroupSkipReason({ rawEntry, eventName, converterConfig }) {
|
|
57163
57374
|
const entry = rawEntry;
|
|
57164
|
-
for (const { tool, valueType, subdividesGroup } of converterConfig.groupPassthroughFields ?? []) {
|
|
57375
|
+
for (const { tool, valueType, subdividesGroup, events, onInvalidImport } of converterConfig.groupPassthroughFields ?? []) {
|
|
57165
57376
|
const value = entry[tool];
|
|
57166
|
-
if (subdividesGroup !== true || value === void 0) continue;
|
|
57377
|
+
if (subdividesGroup !== true || onInvalidImport === "drop-field" || value === void 0 || !isGroupFieldOnEvent(events, eventName)) continue;
|
|
57167
57378
|
if (!isGroupPassthroughValue(value, valueType)) return `Skipping the hooks of a matcher group while importing: its "${tool}" (${quoteValueForWarning(value)}) is unusable, and these hooks run only where it matches. Importing them without it would widen when they fire, so they are skipped.`;
|
|
57168
57379
|
}
|
|
57169
57380
|
}
|
|
57170
57381
|
/**
|
|
57382
|
+
* Warn about each `"drop-field"` group value that is present but unusable;
|
|
57383
|
+
* {@link importGroupPassthroughFields} leaves it off the imported hooks.
|
|
57384
|
+
*/
|
|
57385
|
+
function warnDroppedGroupFields({ rawEntry, eventName, converterConfig, warn }) {
|
|
57386
|
+
const entry = rawEntry;
|
|
57387
|
+
for (const { tool, valueType, events, onInvalidImport } of converterConfig.groupPassthroughFields ?? []) {
|
|
57388
|
+
const value = entry[tool];
|
|
57389
|
+
if (onInvalidImport === "drop-field" && value !== void 0 && isGroupFieldOnEvent(events, eventName) && !isGroupPassthroughValue(value, valueType)) warn?.(`Dropping the unusable "${tool}" (${quoteValueForWarning(value)}) of a "${eventName}" matcher group while importing; its hooks are imported without it.`);
|
|
57390
|
+
}
|
|
57391
|
+
}
|
|
57392
|
+
/**
|
|
57171
57393
|
* Why this hook cannot be imported, or `undefined` when it can.
|
|
57172
57394
|
*/
|
|
57173
57395
|
function describeHookSkipReason({ h, rawEntry, hookType, converterConfig }) {
|
|
@@ -57194,12 +57416,19 @@ function toolMatcherEntryToCanonical({ rawEntry, eventName, converterConfig, war
|
|
|
57194
57416
|
const hookDefs = rawEntry.hooks ?? [];
|
|
57195
57417
|
const groupSkipReason = describeGroupSkipReason({
|
|
57196
57418
|
rawEntry,
|
|
57419
|
+
eventName,
|
|
57197
57420
|
converterConfig
|
|
57198
57421
|
});
|
|
57199
57422
|
if (groupSkipReason !== void 0) {
|
|
57200
57423
|
warn?.(groupSkipReason);
|
|
57201
57424
|
return [];
|
|
57202
57425
|
}
|
|
57426
|
+
warnDroppedGroupFields({
|
|
57427
|
+
rawEntry,
|
|
57428
|
+
eventName,
|
|
57429
|
+
converterConfig,
|
|
57430
|
+
warn
|
|
57431
|
+
});
|
|
57203
57432
|
const definitions = [];
|
|
57204
57433
|
for (const h of hookDefs) {
|
|
57205
57434
|
const skipReason = describeHookSkipReason({
|
|
@@ -60538,6 +60767,23 @@ var CopilotcliHooks = class CopilotcliHooks extends ToolHooks {
|
|
|
60538
60767
|
});
|
|
60539
60768
|
}
|
|
60540
60769
|
};
|
|
60770
|
+
// src/features/hooks/hook-matcher-tool-names.ts
|
|
60771
|
+
const MATCHER_TOKEN_PATTERN = /\\.|\[(?:\\.|[^\]\\])*\]|(?<=^|[^\w\\]|\\\w)[A-Za-z_]\w*(?!\w)/gs;
|
|
60772
|
+
function isMatcherName(token) {
|
|
60773
|
+
return !token.startsWith("\\") && !token.startsWith("[");
|
|
60774
|
+
}
|
|
60775
|
+
/**
|
|
60776
|
+
* Rewrites the whole tool names inside a hook `matcher` regex through `names`,
|
|
60777
|
+
* leaving escapes, character classes and unknown names as written. Used by
|
|
60778
|
+
* adapters whose runtime tool names differ from the canonical (Claude Code)
|
|
60779
|
+
* spelling.
|
|
60780
|
+
*/
|
|
60781
|
+
function replaceMatcherNames(matcher, names) {
|
|
60782
|
+
return matcher.replace(MATCHER_TOKEN_PATTERN, (token) => isMatcherName(token) && Object.hasOwn(names, token) ? names[token] ?? token : token);
|
|
60783
|
+
}
|
|
60784
|
+
function invertToolNames(names) {
|
|
60785
|
+
return Object.fromEntries(Object.entries(names).map(([canonical, tool]) => [tool, canonical]));
|
|
60786
|
+
}
|
|
60541
60787
|
// src/features/hooks/cortexcode-hooks.ts
|
|
60542
60788
|
const CORTEXCODE_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set(["beforeSubmitPrompt", "stop"]);
|
|
60543
60789
|
const CANONICAL_TO_CORTEXCODE_TOOL_NAMES = {
|
|
@@ -60548,14 +60794,7 @@ const CANONICAL_TO_CORTEXCODE_TOOL_NAMES = {
|
|
|
60548
60794
|
Grep: "grep",
|
|
60549
60795
|
Glob: "glob"
|
|
60550
60796
|
};
|
|
60551
|
-
const CORTEXCODE_TO_CANONICAL_TOOL_NAMES =
|
|
60552
|
-
const MATCHER_TOKEN_PATTERN = /\\.|\[(?:\\.|[^\]\\])*\]|(?<=^|[^\w\\]|\\\w)[A-Za-z_]\w*(?!\w)/gs;
|
|
60553
|
-
function isMatcherName(token) {
|
|
60554
|
-
return !token.startsWith("\\") && !token.startsWith("[");
|
|
60555
|
-
}
|
|
60556
|
-
function replaceMatcherNames(matcher, names) {
|
|
60557
|
-
return matcher.replace(MATCHER_TOKEN_PATTERN, (token) => isMatcherName(token) && Object.hasOwn(names, token) ? names[token] ?? token : token);
|
|
60558
|
-
}
|
|
60797
|
+
const CORTEXCODE_TO_CANONICAL_TOOL_NAMES = invertToolNames(CANONICAL_TO_CORTEXCODE_TOOL_NAMES);
|
|
60559
60798
|
function canonicalToCortexcodeMatcher({ matcher, eventName, warn }) {
|
|
60560
60799
|
const translated = replaceMatcherNames(matcher, CANONICAL_TO_CORTEXCODE_TOOL_NAMES);
|
|
60561
60800
|
const unmatched = [...translated.matchAll(MATCHER_TOKEN_PATTERN)].map(([token]) => token).filter((token) => isMatcherName(token) && /^[A-Z]/.test(token));
|
|
@@ -65716,6 +65955,61 @@ var TabnineHooks = class TabnineHooks extends ToolHooks {
|
|
|
65716
65955
|
});
|
|
65717
65956
|
}
|
|
65718
65957
|
};
|
|
65958
|
+
// src/features/hooks/trae-hooks.ts
|
|
65959
|
+
const TRAE_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set([
|
|
65960
|
+
"sessionStart",
|
|
65961
|
+
"beforeSubmitPrompt",
|
|
65962
|
+
"stop"
|
|
65963
|
+
]);
|
|
65964
|
+
const CANONICAL_TO_TRAE_TOOL_NAMES = { Bash: "RunCommand" };
|
|
65965
|
+
const TRAE_TO_CANONICAL_TOOL_NAMES = invertToolNames(CANONICAL_TO_TRAE_TOOL_NAMES);
|
|
65966
|
+
const TRAE_SPEC = {
|
|
65967
|
+
displayName: "TRAE",
|
|
65968
|
+
overrideKey: "trae",
|
|
65969
|
+
converterConfig: {
|
|
65970
|
+
supportedEvents: TRAE_HOOK_EVENTS,
|
|
65971
|
+
canonicalToToolEventNames: CANONICAL_TO_TRAE_EVENT_NAMES,
|
|
65972
|
+
toolToCanonicalEventNames: TRAE_TO_CANONICAL_EVENT_NAMES,
|
|
65973
|
+
projectDirVar: "$TRAE_PROJECT_DIR",
|
|
65974
|
+
stripsProjectDirVarOnImportOnly: true,
|
|
65975
|
+
importedProjectDirVarAliases: ["$CLAUDE_PROJECT_DIR"],
|
|
65976
|
+
noMatcherEvents: TRAE_NO_MATCHER_EVENTS,
|
|
65977
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
65978
|
+
groupPassthroughFields: [{
|
|
65979
|
+
canonical: "loop_limit",
|
|
65980
|
+
tool: "loop_limit",
|
|
65981
|
+
valueType: "number",
|
|
65982
|
+
events: /* @__PURE__ */ new Set(["stop"]),
|
|
65983
|
+
subdividesGroup: true,
|
|
65984
|
+
onInvalidImport: "drop-field"
|
|
65985
|
+
}],
|
|
65986
|
+
matcherTranslation: {
|
|
65987
|
+
toTool: ({ matcher }) => replaceMatcherNames(matcher, CANONICAL_TO_TRAE_TOOL_NAMES),
|
|
65988
|
+
toCanonical: (matcher) => replaceMatcherNames(matcher, TRAE_TO_CANONICAL_TOOL_NAMES)
|
|
65989
|
+
}
|
|
65990
|
+
}
|
|
65991
|
+
};
|
|
65992
|
+
/**
|
|
65993
|
+
* TRAE hooks.
|
|
65994
|
+
*
|
|
65995
|
+
* Hooks live under the top-level `hooks` key of the dedicated `.trae/hooks.json`
|
|
65996
|
+
* (project) and `~/.trae/hooks.json` (user scope). The IDE also writes a
|
|
65997
|
+
* `version` key there, so generation merges the `hooks` key into the file (see
|
|
65998
|
+
* `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it.
|
|
65999
|
+
*
|
|
66000
|
+
* @see https://docs.trae.ai/ide/hook-configuration-reference?_lang=en
|
|
66001
|
+
*/
|
|
66002
|
+
var TraeHooks = class extends SettingsJsonHooks {
|
|
66003
|
+
static getSpec() {
|
|
66004
|
+
return TRAE_SPEC;
|
|
66005
|
+
}
|
|
66006
|
+
static getSettablePaths(_options = {}) {
|
|
66007
|
+
return {
|
|
66008
|
+
relativeDirPath: TRAE_DIR,
|
|
66009
|
+
relativeFilePath: TRAE_HOOKS_FILE_NAME
|
|
66010
|
+
};
|
|
66011
|
+
}
|
|
66012
|
+
};
|
|
65719
66013
|
// src/features/hooks/vibe-hooks.ts
|
|
65720
66014
|
const VIBE_DIR = ".vibe";
|
|
65721
66015
|
const VIBE_HOOKS_FILE_NAME = "hooks.toml";
|
|
@@ -66900,6 +67194,21 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
66900
67194
|
supportedHookTypes: ["command"],
|
|
66901
67195
|
supportsMatcher: true
|
|
66902
67196
|
}],
|
|
67197
|
+
["trae", {
|
|
67198
|
+
class: TraeHooks,
|
|
67199
|
+
meta: {
|
|
67200
|
+
supportsProject: true,
|
|
67201
|
+
supportsGlobal: true,
|
|
67202
|
+
supportsImport: true
|
|
67203
|
+
},
|
|
67204
|
+
...converterCapabilities(TraeHooks.getConverterConfig()),
|
|
67205
|
+
supportsMatcher: true,
|
|
67206
|
+
matcherEvents: [
|
|
67207
|
+
"preToolUse",
|
|
67208
|
+
"postToolUse",
|
|
67209
|
+
"notification"
|
|
67210
|
+
]
|
|
67211
|
+
}],
|
|
66903
67212
|
["vibe", {
|
|
66904
67213
|
class: VibeHooks,
|
|
66905
67214
|
meta: {
|
|
@@ -69008,69 +69317,6 @@ var AiassistantMcp = class extends ToolMcp {
|
|
|
69008
69317
|
});
|
|
69009
69318
|
}
|
|
69010
69319
|
};
|
|
69011
|
-
// src/features/shared/amp-settings.ts
|
|
69012
|
-
/**
|
|
69013
|
-
* Parse an Amp `settings.json` / `settings.jsonc` document.
|
|
69014
|
-
*
|
|
69015
|
-
* Shared by the MCP and permissions adapters because both read the very same
|
|
69016
|
-
* file. Two separately maintained copies drifted once already: the permissions
|
|
69017
|
-
* side was hardened while the MCP side kept calling `jsonc-parser` directly,
|
|
69018
|
-
* which left it importing MCP servers reachable only through an injected
|
|
69019
|
-
* `__proto__`.
|
|
69020
|
-
*
|
|
69021
|
-
* {@link parseJsonc} is the strict, sanitizing parser: it throws on any syntax
|
|
69022
|
-
* error instead of returning `jsonc-parser`'s best-effort value, and it
|
|
69023
|
-
* rebuilds every object from its own enumerable entries, so a root `__proto__`
|
|
69024
|
-
* cannot swap the returned object's prototype and `constructor` / `prototype`
|
|
69025
|
-
* never survive as own keys, at any depth.
|
|
69026
|
-
*
|
|
69027
|
-
* Removal is silent, matching every other tool-side adapter (opencode, kilo,
|
|
69028
|
-
* copilot). Reporting the removed keys through `droppedPollutionKeysError` is
|
|
69029
|
-
* reserved for the three files a user authors under `.rulesync/`, whose whole
|
|
69030
|
-
* purpose is to be turned into tool config — a key that vanishes there needs
|
|
69031
|
-
* explaining, whereas here the surrounding settings are the user's own file
|
|
69032
|
-
* and are left alone.
|
|
69033
|
-
*
|
|
69034
|
-
* Note the consequence for a root `__proto__`: sanitizing runs before the
|
|
69035
|
-
* plain-object check, so such a document parses successfully with the key
|
|
69036
|
-
* stripped rather than failing that check. That is deliberate — dropping the
|
|
69037
|
-
* one poisoned key keeps the user's unrelated settings — and matches the
|
|
69038
|
-
* sanitize-before-the-root-check design the shared config gateway documents.
|
|
69039
|
-
*/
|
|
69040
|
-
function parseAmpSettings({ fileContent }) {
|
|
69041
|
-
let parsed;
|
|
69042
|
-
try {
|
|
69043
|
-
parsed = parseJsonc$6(fileContent || "{}");
|
|
69044
|
-
} catch (error) {
|
|
69045
|
-
throw new Error(`Failed to parse Amp settings: ${formatError(error)}`, { cause: error });
|
|
69046
|
-
}
|
|
69047
|
-
if (!isPlainObject(parsed)) throw new Error("Amp settings must be a JSON object");
|
|
69048
|
-
return parsed;
|
|
69049
|
-
}
|
|
69050
|
-
/**
|
|
69051
|
-
* Probe `<jsonDir>/settings.jsonc` first, falling back to `settings.json`, so
|
|
69052
|
-
* an existing user file is read-modified-written in place instead of a fresh
|
|
69053
|
-
* `.json` sibling being created next to a hand-authored `.jsonc`. Defaults to
|
|
69054
|
-
* `settings.json` when neither file exists: Amp's canonically documented
|
|
69055
|
-
* settings file (`.amp/settings.json`), shared by the MCP and permissions
|
|
69056
|
-
* adapters, so generating both on a clean repo writes a single settings file.
|
|
69057
|
-
*/
|
|
69058
|
-
async function resolveAmpSettingsFile(jsonDir) {
|
|
69059
|
-
const jsoncContent = await readFileContentOrNull((0, node_path.join)(jsonDir, AMP_SETTINGS_JSONC_FILE_NAME));
|
|
69060
|
-
if (jsoncContent !== null) return {
|
|
69061
|
-
fileContent: jsoncContent,
|
|
69062
|
-
relativeFilePath: AMP_SETTINGS_JSONC_FILE_NAME
|
|
69063
|
-
};
|
|
69064
|
-
const jsonContent = await readFileContentOrNull((0, node_path.join)(jsonDir, AMP_SETTINGS_FILE_NAME));
|
|
69065
|
-
if (jsonContent !== null) return {
|
|
69066
|
-
fileContent: jsonContent,
|
|
69067
|
-
relativeFilePath: AMP_SETTINGS_FILE_NAME
|
|
69068
|
-
};
|
|
69069
|
-
return {
|
|
69070
|
-
fileContent: null,
|
|
69071
|
-
relativeFilePath: AMP_SETTINGS_FILE_NAME
|
|
69072
|
-
};
|
|
69073
|
-
}
|
|
69074
69320
|
// src/features/mcp/amp-mcp.ts
|
|
69075
69321
|
const AMP_MCP_SERVERS_KEY = "amp.mcpServers";
|
|
69076
69322
|
function filterMcpServers(mcpServers) {
|
|
@@ -72056,7 +72302,7 @@ var CopilotMcp = class CopilotMcp extends ToolMcp {
|
|
|
72056
72302
|
fileKey: sharedConfigFileKey(paths),
|
|
72057
72303
|
feature: "mcp",
|
|
72058
72304
|
existingContent,
|
|
72059
|
-
patch: { servers: convertToCopilotFormat(mcpServers) },
|
|
72305
|
+
patch: { servers: undefinedWhenEmpty(convertToCopilotFormat(mcpServers)) },
|
|
72060
72306
|
filePath,
|
|
72061
72307
|
logger
|
|
72062
72308
|
}),
|
|
@@ -74377,14 +74623,20 @@ function resolveGooseUrl(config) {
|
|
|
74377
74623
|
}
|
|
74378
74624
|
/**
|
|
74379
74625
|
* Determines the Goose `type` for a server based on its command/url/transport.
|
|
74626
|
+
*
|
|
74627
|
+
* Goose no longer has an SSE extension type (it skips a `type: sse` entry as
|
|
74628
|
+
* unsupported), so every remote server is written as `streamable_http`.
|
|
74380
74629
|
*/
|
|
74381
74630
|
function resolveGooseType(config, url) {
|
|
74382
74631
|
if (config.command !== void 0) return "stdio";
|
|
74383
|
-
if (url !== void 0) return
|
|
74632
|
+
if (url !== void 0) return "streamable_http";
|
|
74384
74633
|
return canonicalTransport(config) === "builtin" ? "builtin" : "stdio";
|
|
74385
74634
|
}
|
|
74386
74635
|
/**
|
|
74387
|
-
* The Goose extension types that carry an MCP server.
|
|
74636
|
+
* The Goose extension types that carry an MCP server. `sse` is no longer one
|
|
74637
|
+
* Goose reads, but it stays here so a stale `type: sse` entry is still
|
|
74638
|
+
* recognized: imported as canonical `sse` and replaced by the regenerated
|
|
74639
|
+
* same-name `streamable_http` entry. Goose also documents
|
|
74388
74640
|
* `builtin`, `platform`, `frontend` and `inline_python` extensions, which have
|
|
74389
74641
|
* no canonical MCP counterpart: they name capabilities Goose provides itself
|
|
74390
74642
|
* rather than a server rulesync could describe.
|
|
@@ -74444,7 +74696,8 @@ function convertServerToGooseExtension(name, config, logger) {
|
|
|
74444
74696
|
warnWithFallback(logger, `Goose extension "${name}" has no command to run; skipping it rather than writing a stdio extension Goose cannot start to ~/.config/goose/config.yaml.`);
|
|
74445
74697
|
return;
|
|
74446
74698
|
}
|
|
74447
|
-
} else if (gooseType === "
|
|
74699
|
+
} else if (gooseType === "streamable_http") {
|
|
74700
|
+
if (canonicalTransport(config) === "sse") warnWithFallback(logger, `Goose extension "${name}" uses SSE, which Goose no longer supports; writing it as streamable_http, which only works if the server also speaks Streamable HTTP.`);
|
|
74448
74701
|
if (url !== void 0) ext.uri = url;
|
|
74449
74702
|
if (isPlainObject(config.headers)) ext.headers = omitPrototypePollutionKeys(config.headers);
|
|
74450
74703
|
}
|
|
@@ -74457,8 +74710,9 @@ function convertServerToGooseExtension(name, config, logger) {
|
|
|
74457
74710
|
* Converts rulesync canonical MCP servers into Goose `extensions:` entries.
|
|
74458
74711
|
*
|
|
74459
74712
|
* Goose uses a non-standard schema: `name`, `type` (`stdio` | `streamable_http`
|
|
74460
|
-
* | `
|
|
74461
|
-
*
|
|
74713
|
+
* | `builtin`), `cmd`/`args`/`envs` for stdio, `uri`/`headers` for remote, plus
|
|
74714
|
+
* `enabled` and `timeout`. A canonical `sse` server is written as
|
|
74715
|
+
* `streamable_http` with a warning, since Goose dropped SSE support.
|
|
74462
74716
|
*
|
|
74463
74717
|
* `extensions:` is co-owned: alongside the MCP servers rulesync manages it also
|
|
74464
74718
|
* holds Goose's own `builtin`/`platform`/`frontend`/`inline_python` extensions
|
|
@@ -74589,7 +74843,8 @@ function convertToGoosePluginMcpServers(mcpServers, logger) {
|
|
|
74589
74843
|
* `~/.config/goose/config.yaml`. That file also holds other Goose settings
|
|
74590
74844
|
* (model, provider, ...), so generation merges the `extensions:` block into the
|
|
74591
74845
|
* existing config instead of overwriting it, and the file is never deleted.
|
|
74592
|
-
* This location supports both stdio and remote (
|
|
74846
|
+
* This location supports both stdio and remote servers (SSE is written as
|
|
74847
|
+
* `streamable_http`, since Goose no longer reads `type: sse`).
|
|
74593
74848
|
* - **Project**: a stdio-only open-plugin manifest at
|
|
74594
74849
|
* `.agents/plugins/rulesync/.mcp.json` (Goose v1.39.0+), reusing the same
|
|
74595
74850
|
* `.agents/plugins/rulesync/` tree as Goose hooks. The manifest uses the
|
|
@@ -79307,6 +79562,7 @@ var VibeMcp = class VibeMcp extends ToolMcp {
|
|
|
79307
79562
|
const managedNames = new Set(Object.keys(rulesyncServers));
|
|
79308
79563
|
const mcpServers = Object.entries(rulesyncServers).map(([name, server]) => rulesyncMcpServerToVibe(name, server, existingByName.get(name)));
|
|
79309
79564
|
const unmanagedServers = existingServers.filter((server) => !managedNames.has(server.name));
|
|
79565
|
+
const servers = [...mcpServers, ...unmanagedServers];
|
|
79310
79566
|
return new VibeMcp({
|
|
79311
79567
|
outputRoot,
|
|
79312
79568
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -79315,7 +79571,7 @@ var VibeMcp = class VibeMcp extends ToolMcp {
|
|
|
79315
79571
|
fileKey: sharedConfigFileKey(paths),
|
|
79316
79572
|
feature: "mcp",
|
|
79317
79573
|
existingContent,
|
|
79318
|
-
patch: { mcp_servers:
|
|
79574
|
+
patch: { mcp_servers: servers.length > 0 ? servers : void 0 },
|
|
79319
79575
|
filePath
|
|
79320
79576
|
}),
|
|
79321
79577
|
validate,
|
|
@@ -82739,6 +82995,20 @@ function isUnicodeClass(rest) {
|
|
|
82739
82995
|
}));
|
|
82740
82996
|
}
|
|
82741
82997
|
/**
|
|
82998
|
+
* Return a function that, for a `[:` at `index`, gives the position of the
|
|
82999
|
+
* next `:]` (Go reads everything between as the class name), or -1. The
|
|
83000
|
+
* position is cached and `index` only grows, so a long run of `[:` with no
|
|
83001
|
+
* `:]` costs linear time rather than a scan to the end at each one.
|
|
83002
|
+
*/
|
|
83003
|
+
function posixEndFinder(source) {
|
|
83004
|
+
let next = -2;
|
|
83005
|
+
return (index) => {
|
|
83006
|
+
if (!source.startsWith("[:", index)) return -1;
|
|
83007
|
+
if (next !== -1 && next < index + 2) next = source.indexOf(":]", index + 2);
|
|
83008
|
+
return next;
|
|
83009
|
+
};
|
|
83010
|
+
}
|
|
83011
|
+
/**
|
|
82742
83012
|
* Replace each POSIX class (`[:alpha:]`) inside a bracket class with a plain
|
|
82743
83013
|
* character, so JavaScript can compile the rest, or return `undefined` for an
|
|
82744
83014
|
* unknown one. Only inside a bracket is `[:name:]` a POSIX class; a bare
|
|
@@ -82748,6 +83018,7 @@ function replacePosixClasses(source) {
|
|
|
82748
83018
|
let result = "";
|
|
82749
83019
|
let inClass = false;
|
|
82750
83020
|
let index = 0;
|
|
83021
|
+
const findPosixEnd = posixEndFinder(source);
|
|
82751
83022
|
while (index < source.length) {
|
|
82752
83023
|
const character = source[index] ?? "";
|
|
82753
83024
|
if (character === "\\") {
|
|
@@ -82772,11 +83043,11 @@ function replacePosixClasses(source) {
|
|
|
82772
83043
|
}
|
|
82773
83044
|
continue;
|
|
82774
83045
|
}
|
|
82775
|
-
const
|
|
82776
|
-
if (
|
|
82777
|
-
if (!POSIX_CLASSES.has(
|
|
83046
|
+
const posixEnd = findPosixEnd(index);
|
|
83047
|
+
if (posixEnd !== -1) {
|
|
83048
|
+
if (!POSIX_CLASSES.has(source.slice(index + 2, posixEnd).replace(/^\^/, ""))) return;
|
|
82778
83049
|
result += "a";
|
|
82779
|
-
index
|
|
83050
|
+
index = posixEnd + 2;
|
|
82780
83051
|
continue;
|
|
82781
83052
|
}
|
|
82782
83053
|
if (character === "]") inClass = false;
|
|
@@ -82855,6 +83126,7 @@ function hasClassEscapeRangeEnd(source) {
|
|
|
82855
83126
|
let inClass = false;
|
|
82856
83127
|
let afterCharacter = false;
|
|
82857
83128
|
let index = 0;
|
|
83129
|
+
const findPosixEnd = posixEndFinder(source);
|
|
82858
83130
|
while (index < source.length) {
|
|
82859
83131
|
const character = source[index] ?? "";
|
|
82860
83132
|
if (character === "\\") {
|
|
@@ -82870,7 +83142,7 @@ function hasClassEscapeRangeEnd(source) {
|
|
|
82870
83142
|
});
|
|
82871
83143
|
continue;
|
|
82872
83144
|
}
|
|
82873
|
-
const posixEnd =
|
|
83145
|
+
const posixEnd = findPosixEnd(index);
|
|
82874
83146
|
if (character === "]" || posixEnd !== -1) {
|
|
82875
83147
|
inClass = character !== "]";
|
|
82876
83148
|
afterCharacter = false;
|
|
@@ -83432,6 +83704,8 @@ function buildToolPatternEntry({ toolName, pattern }) {
|
|
|
83432
83704
|
* - `execute_url` takes a domain, like `read_url`.
|
|
83433
83705
|
* - `command` and `unsandboxed` take literal words or a per-word `regex:`,
|
|
83434
83706
|
* matched word by word (see `antigravity-command-patterns.ts`).
|
|
83707
|
+
* - `mcp` takes `server/tool` or `server/*`, matched exactly. A canonical
|
|
83708
|
+
* `mcp__<server>__<tool>` category is written as that target.
|
|
83435
83709
|
*
|
|
83436
83710
|
* A glob copied verbatim (`read_file(**\/*.env)`) is a literal path that
|
|
83437
83711
|
* matches nothing, so a deny written that way blocks nothing. A pattern with
|
|
@@ -83455,7 +83729,8 @@ const DENY_HINTS = {
|
|
|
83455
83729
|
read_file: "deny a directory or file path instead",
|
|
83456
83730
|
write_file: "deny a directory or file path instead",
|
|
83457
83731
|
command: "deny the first words of the command instead",
|
|
83458
|
-
unsandboxed: "deny the first words of the command instead"
|
|
83732
|
+
unsandboxed: "deny the first words of the command instead",
|
|
83733
|
+
mcp: "deny the whole MCP server or tool instead"
|
|
83459
83734
|
};
|
|
83460
83735
|
function hasGlob(pattern) {
|
|
83461
83736
|
return BRACE_ALTERNATIVES.test(pattern) || parseGlobPattern(pattern).steps.some((step) => step.kind !== "literal");
|
|
@@ -83511,6 +83786,40 @@ function toTarget(action, pattern) {
|
|
|
83511
83786
|
if (URL_ACTIONS.has(action)) return toUrlTarget(pattern);
|
|
83512
83787
|
return { target: pattern };
|
|
83513
83788
|
}
|
|
83789
|
+
const MCP_CATEGORY_PREFIX = "mcp__";
|
|
83790
|
+
const MCP_TARGET_SEPARATORS = /[/()]/;
|
|
83791
|
+
/**
|
|
83792
|
+
* The `mcp` target of a canonical `mcp__<server>__<tool>` category:
|
|
83793
|
+
* `<server>/<tool>`, `<server>/*` for a whole server (`mcp__<server>`,
|
|
83794
|
+
* `mcp__<server>__*`), or `*` for every MCP tool (`mcp__*`, `mcp__*__*`). The
|
|
83795
|
+
* server ends at the first `__`, as elsewhere in rulesync. Antigravity matches
|
|
83796
|
+
* names exactly, so a glob, an empty name, or a `/` or parenthesis in a name
|
|
83797
|
+
* has no target; when only the tool part is at fault, its server is the `lead`
|
|
83798
|
+
* a deny widens to.
|
|
83799
|
+
*/
|
|
83800
|
+
function toMcpTarget(category) {
|
|
83801
|
+
const remainder = category.slice(5);
|
|
83802
|
+
const separator = remainder.indexOf("__");
|
|
83803
|
+
const server = separator === -1 ? remainder : remainder.slice(0, separator);
|
|
83804
|
+
const tool = separator === -1 ? "*" : remainder.slice(separator + 2);
|
|
83805
|
+
if (server === "*" && tool === "*") return { target: "*" };
|
|
83806
|
+
const skipReason = "Antigravity MCP targets name one server or one tool exactly. Only `mcp__<server>`, `mcp__<server>__*`, `mcp__<server>__<tool>` or `mcp__*` can be expressed";
|
|
83807
|
+
if (server.length === 0 || MCP_TARGET_SEPARATORS.test(server) || hasGlob(server)) return { skipReason };
|
|
83808
|
+
if (tool === "*") return { target: `${server}/*` };
|
|
83809
|
+
return tool.length === 0 || MCP_TARGET_SEPARATORS.test(tool) || hasGlob(tool) ? {
|
|
83810
|
+
skipReason,
|
|
83811
|
+
lead: `${server}/*`
|
|
83812
|
+
} : { target: `${server}/${tool}` };
|
|
83813
|
+
}
|
|
83814
|
+
/** The target of a canonical `mcp__...` rule, with the `lead` a deny widens to. */
|
|
83815
|
+
function toMcpRuleTarget(category, pattern) {
|
|
83816
|
+
const target = toMcpTarget(category);
|
|
83817
|
+
if (pattern === "*" || !("target" in target)) return target;
|
|
83818
|
+
return {
|
|
83819
|
+
skipReason: "Antigravity MCP targets name a server or a tool, not the tool's arguments",
|
|
83820
|
+
lead: target.target
|
|
83821
|
+
};
|
|
83822
|
+
}
|
|
83514
83823
|
/**
|
|
83515
83824
|
* The leading directories of a path glob (`src` for `src/*.ts`), or
|
|
83516
83825
|
* `undefined` when it has none. A directory target covers everything inside
|
|
@@ -83533,6 +83842,20 @@ function toWidenedDenyTarget(action, pattern) {
|
|
|
83533
83842
|
if (ANTIGRAVITY_COMMAND_ACTIONS.has(action)) return toAntigravityCommandLiteralPrefix(pattern);
|
|
83534
83843
|
if (PATH_ACTIONS.has(action)) return toPathLiteralPrefix(pattern);
|
|
83535
83844
|
}
|
|
83845
|
+
/** The entry a deny is widened to, and what it now blocks. */
|
|
83846
|
+
function describeWidenedDeny({ action, prefix, isMcp }) {
|
|
83847
|
+
if (isMcp) return prefix === "*" ? {
|
|
83848
|
+
entry: action,
|
|
83849
|
+
scope: "every MCP tool"
|
|
83850
|
+
} : {
|
|
83851
|
+
entry: `${action}(${prefix})`,
|
|
83852
|
+
scope: `every call of \`${action}(${prefix})\``
|
|
83853
|
+
};
|
|
83854
|
+
return {
|
|
83855
|
+
entry: `${action}(${prefix})`,
|
|
83856
|
+
scope: PATH_ACTIONS.has(action) ? `every path under \`${prefix}\`` : `every command that starts with \`${prefix}\``
|
|
83857
|
+
};
|
|
83858
|
+
}
|
|
83536
83859
|
/**
|
|
83537
83860
|
* Warn about a path target that is a home or variable reference. Antigravity
|
|
83538
83861
|
* documents absolute and workspace-relative paths only, so such a reference
|
|
@@ -83554,14 +83877,20 @@ function warnHomeOrVariablePath({ action, target, rule, decision, logger, toolLa
|
|
|
83554
83877
|
* nothing enforces it, and a widened one is warned about too, because it
|
|
83555
83878
|
* blocks more than it says.
|
|
83556
83879
|
*/
|
|
83557
|
-
function buildAntigravityPermissionEntry({ action, category, pattern, decision, logger, toolLabel }) {
|
|
83558
|
-
const
|
|
83880
|
+
function buildAntigravityPermissionEntry({ action: toolAction, category, pattern, decision, logger, toolLabel }) {
|
|
83881
|
+
const isMcp = category.startsWith(MCP_CATEGORY_PREFIX);
|
|
83882
|
+
const action = isMcp ? "mcp" : toolAction;
|
|
83883
|
+
const result = isMcp ? toMcpRuleTarget(category, pattern) : pattern === "*" ? { target: "*" } : toTarget(action, pattern);
|
|
83559
83884
|
const rule = `${category}: { "${pattern}": "${decision}" }`;
|
|
83560
83885
|
if ("skipReason" in result) {
|
|
83561
|
-
const prefix = decision
|
|
83886
|
+
const prefix = decision !== "deny" ? void 0 : isMcp ? result.lead : toWidenedDenyTarget(action, pattern);
|
|
83562
83887
|
if (prefix !== void 0) {
|
|
83563
|
-
const
|
|
83564
|
-
|
|
83888
|
+
const { entry, scope } = describeWidenedDeny({
|
|
83889
|
+
action,
|
|
83890
|
+
prefix,
|
|
83891
|
+
isMcp
|
|
83892
|
+
});
|
|
83893
|
+
logger.warn(`${toolLabel} permissions: widening ${rule} to ${entry}. ${result.skipReason}. The deny now blocks ${scope}, overriding any allow or ask for them.`);
|
|
83565
83894
|
warnHomeOrVariablePath({
|
|
83566
83895
|
action,
|
|
83567
83896
|
target: prefix,
|
|
@@ -83570,7 +83899,7 @@ function buildAntigravityPermissionEntry({ action, category, pattern, decision,
|
|
|
83570
83899
|
logger,
|
|
83571
83900
|
toolLabel
|
|
83572
83901
|
});
|
|
83573
|
-
return
|
|
83902
|
+
return entry;
|
|
83574
83903
|
}
|
|
83575
83904
|
const consequence = decision === "deny" ? `This deny is NOT enforced in ${toolLabel}; ${DENY_HINTS[action] ?? "write it in a form Antigravity can match"}` : decision === "ask" ? `${toolLabel} falls back to its default for it, so it runs without a prompt when ${toolLabel} is set to always proceed` : `${toolLabel} falls back to its default for it`;
|
|
83576
83905
|
logger.warn(`${toolLabel} permissions: skipping ${rule}. ${result.skipReason}. ${consequence}.`);
|
|
@@ -83714,6 +84043,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
|
|
|
83714
84043
|
mcp: "mcp"
|
|
83715
84044
|
};
|
|
83716
84045
|
function toAntigravityCliToolName(canonical) {
|
|
84046
|
+
if (canonical.startsWith("mcp__")) return "mcp";
|
|
83717
84047
|
return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
|
|
83718
84048
|
}
|
|
83719
84049
|
function toCanonicalToolName$9(cliName) {
|
|
@@ -83776,10 +84106,11 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
|
|
|
83776
84106
|
logger
|
|
83777
84107
|
});
|
|
83778
84108
|
const managedToolNames = new Set(Object.keys(config.permission).map((category) => toAntigravityCliToolName(category)));
|
|
84109
|
+
const isManagedToolName = (toolName) => managedToolNames.has(toolName) || managedToolNames.has("mcp") && toolName.startsWith("mcp__");
|
|
83779
84110
|
const existingPermissions = settings.permissions ?? {};
|
|
83780
|
-
const preservedAllow = (existingPermissions.allow ?? []).filter((entry) => !
|
|
83781
|
-
const preservedAsk = (existingPermissions.ask ?? []).filter((entry) => !
|
|
83782
|
-
const preservedDeny = (existingPermissions.deny ?? []).filter((entry) => !
|
|
84111
|
+
const preservedAllow = (existingPermissions.allow ?? []).filter((entry) => !isManagedToolName(parseToolPatternEntry(entry).toolName));
|
|
84112
|
+
const preservedAsk = (existingPermissions.ask ?? []).filter((entry) => !isManagedToolName(parseToolPatternEntry(entry).toolName));
|
|
84113
|
+
const preservedDeny = (existingPermissions.deny ?? []).filter((entry) => !isManagedToolName(parseToolPatternEntry(entry).toolName));
|
|
83783
84114
|
const mergedPermissions = { ...existingPermissions };
|
|
83784
84115
|
const mergedAllow = (0, es_toolkit.uniq)([...preservedAllow, ...allow].toSorted());
|
|
83785
84116
|
const mergedAsk = (0, es_toolkit.uniq)([...preservedAsk, ...ask].toSorted());
|
|
@@ -83888,6 +84219,7 @@ const IDE_ACTION_TO_CANONICAL = {
|
|
|
83888
84219
|
mcp: "mcp"
|
|
83889
84220
|
};
|
|
83890
84221
|
function toIdeAction(canonical) {
|
|
84222
|
+
if (canonical.startsWith("mcp__")) return "mcp";
|
|
83891
84223
|
return CANONICAL_TO_IDE_ACTION[canonical] ?? canonical;
|
|
83892
84224
|
}
|
|
83893
84225
|
function toCanonicalCategory$2(ideAction) {
|
|
@@ -83944,8 +84276,9 @@ var AntigravityIdePermissions = class AntigravityIdePermissions extends ToolPerm
|
|
|
83944
84276
|
logger
|
|
83945
84277
|
});
|
|
83946
84278
|
const managedActions = new Set(Object.keys(config.permission).map((category) => toIdeAction(category)));
|
|
84279
|
+
const isManagedAction = (action) => managedActions.has(action) || managedActions.has("mcp") && action.startsWith("mcp__");
|
|
83947
84280
|
const existingPermissions = settings.permissions ?? {};
|
|
83948
|
-
const preserve = (entries) => (entries ?? []).filter((entry) => !
|
|
84281
|
+
const preserve = (entries) => (entries ?? []).filter((entry) => !isManagedAction(parseToolPatternEntry(entry).toolName));
|
|
83949
84282
|
const mergedPermissions = { ...existingPermissions };
|
|
83950
84283
|
const mergedAllow = (0, es_toolkit.uniq)([...preserve(existingPermissions.allow), ...allow].toSorted());
|
|
83951
84284
|
const mergedAsk = (0, es_toolkit.uniq)([...preserve(existingPermissions.ask), ...ask].toSorted());
|