Skip to content

Commit c9a462b

Browse files
committed
feat: add Trae hook support
1 parent 07c5b64 commit c9a462b

22 files changed

Lines changed: 514 additions & 30 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to @rpamis/comet will be documented in this file.
44

5+
## What's Changed [0.4.0-beta.17] - 2026-08-06
6+
7+
### Added
8+
9+
- **Trae Hook support**: `comet init`, `comet update`, `comet doctor`, and `comet uninstall` now support managed Hook Router entries for Trae and Trae CN, using Trae's official project and global `hooks.json` locations while preserving user-owned Hook configuration.
10+
511
## What's Changed [0.4.0-beta.16] - 2026-08-05
612

713
### Fixed

assets/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "0.4.0-beta.16",
2+
"version": "0.4.0-beta.17",
33
"skills": [
44
"comet/SKILL.md",
55
"comet-classic/SKILL.md",

assets/skills/comet/scripts/comet-hook-router.mjs

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

domains/bundle/bundle-platform.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ function hookDestination(target: BundlePlatformTarget, hookId: string): string |
118118
case 'gemini':
119119
return path.join(platformRoot, 'settings.json');
120120
case 'windsurf':
121+
case 'trae':
121122
return path.join(platformRoot, 'hooks.json');
122123
case 'copilot':
123124
return path.join(platformRoot, 'hooks', `${hookId}.json`);
@@ -291,6 +292,18 @@ async function applyHookInstallFile(file: PlatformInstallFile): Promise<void> {
291292
await writeFile(file.destination, JSON.stringify(settings, null, 2) + '\n');
292293
return;
293294
}
295+
case 'trae': {
296+
hooks.PreToolUse = mergeCommandHookGroup(
297+
asHookGroups(hooks.PreToolUse),
298+
matcher,
299+
{ ...commandHook, timeout: 30 },
300+
operation.command,
301+
);
302+
settings.version = settings.version ?? 1;
303+
settings.hooks = hooks;
304+
await writeFile(file.destination, JSON.stringify(settings, null, 2) + '\n');
305+
return;
306+
}
294307
case 'gemini': {
295308
const geminiMatcher = matcher === 'Write|Edit' ? 'write_file|edit_file' : matcher;
296309
hooks.BeforeTool = mergeCommandHookGroup(

domains/bundle/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,8 @@ export interface PlatformInstallFile {
334334
| 'qwen'
335335
| 'kiro'
336336
| 'qoder'
337-
| 'codebuddy';
337+
| 'codebuddy'
338+
| 'trae';
338339
event: NormalizedHook['event'];
339340
matcher?: string;
340341
command: string;

domains/comet-entry/hook-adapter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export const COMET_HOOK_PLATFORM_IDS = new Set([
4343
'kiro',
4444
'codebuddy',
4545
'qoder',
46+
'trae',
47+
'trae-cn',
4648
]);
4749

4850
function isRecord(value: unknown): value is Record<string, unknown> {

domains/skill/platform-inspect.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ function countGroupedHookMatches(
117117
groupName: string,
118118
expected: ExpectedHookDescriptor,
119119
expectedMatcher: (matcher: string) => string = (matcher) => matcher,
120+
isExpectedHandler: (
121+
handler: Record<string, unknown>,
122+
expected: ExpectedHookDescriptor,
123+
) => boolean = (handler, descriptor) =>
124+
handler.type === 'command' && handler.command === descriptor.command,
120125
): number {
121126
const hooks = config.hooks;
122127
if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks)) return 0;
@@ -135,8 +140,7 @@ function countGroupedHookMatches(
135140
handler !== null &&
136141
typeof handler === 'object' &&
137142
!Array.isArray(handler) &&
138-
(handler as Record<string, unknown>).type === 'command' &&
139-
(handler as Record<string, unknown>).command === expected.command,
143+
isExpectedHandler(handler as Record<string, unknown>, expected),
140144
).length
141145
);
142146
}, 0);
@@ -440,6 +444,29 @@ export async function inspectCometHooksForPlatform(
440444
countWindsurfHookMatches,
441445
);
442446
break;
447+
case 'trae':
448+
inspection = await inspectSingleHookJson(
449+
path.join(platformBase, 'hooks.json'),
450+
expectedHooks,
451+
(config) => collectGroupedCommands(config, 'PreToolUse'),
452+
(config, expected) =>
453+
countGroupedHookMatches(
454+
config,
455+
'PreToolUse',
456+
expected,
457+
undefined,
458+
(handler, descriptor) => {
459+
const timeout = handler.timeout;
460+
return (
461+
handler.type === 'command' &&
462+
handler.command === descriptor.command &&
463+
typeof timeout === 'number' &&
464+
timeout > 0
465+
);
466+
},
467+
),
468+
);
469+
break;
443470
case 'copilot':
444471
inspection = await inspectSingleHookJson(
445472
path.join(platformBase, 'hooks', 'comet-guard.json'),

domains/skill/platform-install.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,7 @@ ${content}`;
10061006
* 'windsurf' — hooks.json with pre_write_code array
10071007
* 'copilot' — hooks/*.json with preToolUse
10081008
* 'kiro' — hooks/*.kiro.hook JSON files
1009+
* 'trae' — hooks.json with version and PreToolUse grouped command hooks
10091010
*/
10101011
async function installCometHooksForPlatform(
10111012
baseDir: string,
@@ -1023,7 +1024,7 @@ async function installCometHooksForPlatform(
10231024
};
10241025
}
10251026

1026-
if (scope === 'global') {
1027+
if (scope === 'global' && platform.hookFormat !== 'trae') {
10271028
return {
10281029
status: 'skipped',
10291030
reason: 'blocking Hooks are project-scoped',
@@ -1114,6 +1115,15 @@ async function installCometHooksForPlatform(
11141115
platformId: platform.id,
11151116
scope,
11161117
});
1118+
case 'trae':
1119+
return await installTraeHooks(
1120+
baseDir,
1121+
platformBase,
1122+
skillsDir,
1123+
hooksConfig,
1124+
platform.name,
1125+
{ platformId: platform.id, scope },
1126+
);
11171127
default:
11181128
return { status: 'failed', reason: `unsupported hook format: ${hookFormat}` };
11191129
}
@@ -1542,6 +1552,53 @@ async function installWindsurfHooks(
15421552
return { status: 'installed' };
15431553
}
15441554

1555+
/**
1556+
* Trae format:
1557+
* Writes to hooks.json with { version: 1, hooks: { PreToolUse: [{ matcher, hooks: [{ type, command, timeout }] }] } }
1558+
*/
1559+
async function installTraeHooks(
1560+
baseDir: string,
1561+
platformBase: string,
1562+
skillsDir: string,
1563+
hooksConfig: Record<string, HookConfig>,
1564+
platformName: string,
1565+
context: HookCommandContext,
1566+
): Promise<HookInstallResult> {
1567+
const hooksPath = path.join(platformBase, 'hooks.json');
1568+
const matcherGroups: Record<
1569+
string,
1570+
Array<{ type: string; command: string; timeout: number }>
1571+
> = {};
1572+
1573+
for (const [scriptRelPath, config] of Object.entries(hooksConfig)) {
1574+
matcherGroups[config.matcher] ??= [];
1575+
matcherGroups[config.matcher].push({
1576+
type: 'command',
1577+
command: buildHookCommand(baseDir, skillsDir, scriptRelPath, context),
1578+
timeout: 30,
1579+
});
1580+
}
1581+
1582+
const preToolUseEntries = Object.entries(matcherGroups).map(([matcher, hooks]) => ({
1583+
matcher,
1584+
hooks,
1585+
}));
1586+
const hooksFile = await readSettingsJsonObject(hooksPath, platformName);
1587+
const existingHooks = (hooksFile.hooks as Record<string, unknown>) ?? {};
1588+
const existingPreToolUse = asHookGroup(existingHooks.PreToolUse);
1589+
const merged = mergeHookGroups(
1590+
existingPreToolUse,
1591+
preToolUseEntries,
1592+
managedHookScriptPaths(hooksConfig),
1593+
);
1594+
1595+
hooksFile.version = hooksFile.version ?? 1;
1596+
hooksFile.hooks = { ...existingHooks, PreToolUse: merged };
1597+
await ensureDir(path.dirname(hooksPath));
1598+
await writeFile(hooksPath, JSON.stringify(hooksFile, null, 2) + '\n', 'utf-8');
1599+
return { status: 'installed' };
1600+
}
1601+
15451602
/**
15461603
* GitHub Copilot format:
15471604
* Writes to .github/hooks/comet-guard.json with preToolUse hooks config.

domains/skill/uninstall.ts

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,8 @@ async function removeCometHooksForPlatform(
10471047
return await removeGeminiHooks(platformBase, scriptRelPaths);
10481048
case 'windsurf':
10491049
return await removeWindsurfHooks(platformBase, scriptRelPaths);
1050+
case 'trae':
1051+
return await removeTraeHooks(platformBase, scriptRelPaths);
10501052
case 'copilot':
10511053
return await removeCopilotHooks(platformBase, scriptRelPaths);
10521054
case 'kiro':
@@ -1065,7 +1067,6 @@ async function removeQwenStyleHooks(
10651067
): Promise<RemovalResult> {
10661068
const settingsPath = path.join(platformBase, 'settings.json');
10671069
if (!(await fileExists(settingsPath))) return { removed: 0, failed: 0 };
1068-
let removed = 0;
10691070
const readResult = await readJsonObjectFile(settingsPath);
10701071
if (readResult.status === 'missing') return { removed: 0, failed: 0 };
10711072
if (readResult.status === 'error') return { removed: 0, failed: 1 };
@@ -1081,6 +1082,7 @@ async function removeQwenStyleHooks(
10811082
return { removed: 0, failed: 0 };
10821083
}
10831084

1085+
let removed = 0;
10841086
const filtered = existingPreToolUse.flatMap((group) => {
10851087
if (!Array.isArray(group.hooks)) return [group];
10861088

@@ -1117,7 +1119,6 @@ async function removeGeminiHooks(
11171119
): Promise<RemovalResult> {
11181120
const settingsPath = path.join(platformBase, 'settings.json');
11191121
if (!(await fileExists(settingsPath))) return { removed: 0, failed: 0 };
1120-
let removed = 0;
11211122
const readResult = await readJsonObjectFile(settingsPath);
11221123
if (readResult.status === 'missing') return { removed: 0, failed: 0 };
11231124
if (readResult.status === 'error') return { removed: 0, failed: 1 };
@@ -1133,7 +1134,30 @@ async function removeGeminiHooks(
11331134
return { removed: 0, failed: 0 };
11341135
}
11351136

1136-
const filtered = existingBeforeTool.flatMap((group) => {
1137+
const result = removeManagedGroupedHooks(existingBeforeTool, scriptRelPaths);
1138+
const removed = result.removed;
1139+
const filtered = result.groups;
1140+
1141+
if (filtered.length === 0) {
1142+
delete existingHooks.BeforeTool;
1143+
} else {
1144+
existingHooks.BeforeTool = filtered;
1145+
}
1146+
1147+
if (Object.keys(existingHooks).length === 0) {
1148+
delete settings.hooks;
1149+
}
1150+
1151+
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
1152+
return { removed, failed: 0 };
1153+
}
1154+
1155+
function removeManagedGroupedHooks(
1156+
groups: Array<Record<string, unknown>>,
1157+
scriptRelPaths: string[],
1158+
): { groups: Array<Record<string, unknown>>; removed: number } {
1159+
let removed = 0;
1160+
const filtered = groups.flatMap((group) => {
11371161
if (!Array.isArray(group.hooks)) return [group];
11381162

11391163
const hooksBefore = (group.hooks as Array<Record<string, unknown>>).length;
@@ -1149,18 +1173,7 @@ async function removeGeminiHooks(
11491173
return [{ ...group, hooks }];
11501174
});
11511175

1152-
if (filtered.length === 0) {
1153-
delete existingHooks.BeforeTool;
1154-
} else {
1155-
existingHooks.BeforeTool = filtered;
1156-
}
1157-
1158-
if (Object.keys(existingHooks).length === 0) {
1159-
delete settings.hooks;
1160-
}
1161-
1162-
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
1163-
return { removed, failed: 0 };
1176+
return { groups: filtered, removed };
11641177
}
11651178

11661179
async function removeWindsurfHooks(
@@ -1169,7 +1182,6 @@ async function removeWindsurfHooks(
11691182
): Promise<RemovalResult> {
11701183
const hooksPath = path.join(platformBase, 'hooks.json');
11711184
if (!(await fileExists(hooksPath))) return { removed: 0, failed: 0 };
1172-
let removed = 0;
11731185
const readResult = await readJsonObjectFile(hooksPath);
11741186
if (readResult.status === 'missing') return { removed: 0, failed: 0 };
11751187
if (readResult.status === 'error') return { removed: 0, failed: 1 };
@@ -1187,6 +1199,7 @@ async function removeWindsurfHooks(
11871199
return { removed: 0, failed: 0 };
11881200
}
11891201

1202+
let removed = 0;
11901203
const filtered = existingPreWrite.filter((entry) => {
11911204
if (isManagedHookCommand(entry.command, scriptRelPaths)) {
11921205
removed++;
@@ -1209,6 +1222,45 @@ async function removeWindsurfHooks(
12091222
return { removed, failed: 0 };
12101223
}
12111224

1225+
async function removeTraeHooks(
1226+
platformBase: string,
1227+
scriptRelPaths: string[],
1228+
): Promise<RemovalResult> {
1229+
const hooksPath = path.join(platformBase, 'hooks.json');
1230+
if (!(await fileExists(hooksPath))) return { removed: 0, failed: 0 };
1231+
const readResult = await readJsonObjectFile(hooksPath);
1232+
if (readResult.status === 'missing') return { removed: 0, failed: 0 };
1233+
if (readResult.status === 'error') return { removed: 0, failed: 1 };
1234+
const hooksFile = readResult.value;
1235+
1236+
const existingHooks = hooksFile.hooks as Record<string, unknown> | undefined;
1237+
if (!existingHooks) {
1238+
return { removed: 0, failed: 0 };
1239+
}
1240+
1241+
const existingPreToolUse = existingHooks.PreToolUse as Array<Record<string, unknown>> | undefined;
1242+
if (!existingPreToolUse || !Array.isArray(existingPreToolUse)) {
1243+
return { removed: 0, failed: 0 };
1244+
}
1245+
1246+
const result = removeManagedGroupedHooks(existingPreToolUse, scriptRelPaths);
1247+
const removed = result.removed;
1248+
const filtered = result.groups;
1249+
1250+
if (filtered.length === 0) {
1251+
delete existingHooks.PreToolUse;
1252+
} else {
1253+
existingHooks.PreToolUse = filtered;
1254+
}
1255+
1256+
if (Object.keys(existingHooks).length === 0) {
1257+
delete hooksFile.hooks;
1258+
}
1259+
1260+
await writeFile(hooksPath, JSON.stringify(hooksFile, null, 2) + '\n', 'utf-8');
1261+
return { removed, failed: 0 };
1262+
}
1263+
12121264
async function removeCopilotHooks(
12131265
platformBase: string,
12141266
scriptRelPaths: string[],

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)