feat(clients): register Trae during setup (#1573)

* feat(clients): register Trae during setup

- SupportedClients: trae definition (VS Code-style user directory,
  User/mcp.json, format json)
- Trae documents mcpServers as the top-level server table with
  command/args/env and stdio as the default transport; it has no
  personal skills directory, so setup skips the skill copy
- tests: traeClientConfiguration.test.ts (per-platform user directory
  resolution, upsert preserving unrelated settings, fresh/empty
  configs, uninstall)

Verified: tsc --noEmit clean; vitest run
tests/boundary/filesystem/traeClientConfiguration.test.ts 6/6.

* fix(clients): install Trae's personal REA skill

Review follow-up (#1573): the Trae Skills documentation lists global
skills under ~/.trae/skills on macOS/Linux and %userprofile%/.trae/skills
on Windows, so the definition now carries that skillPath and setup
installs the bundled skill copy there. The installation docs no longer
claim Trae has no personal skills directory, and the bundled REA
skill's supported client list includes trae.

Verified: tsc --noEmit clean; vitest run
tests/boundary/filesystem/traeClientConfiguration.test.ts 7/7.

* docs(clients): qualify Trae configuration path evidence

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
Iven Koo 2026-10-12 22:53:30 +08:00 committed by GitHub
parent 51d6c87cdb
commit bfa805decf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 216 additions and 1 deletions

View File

@ -45,7 +45,7 @@ rea setup --client codex --dry-run --json
Use the current supported client's ID: `claude_code`, `claude_desktop`, `codex`,
`cursor`, `gemini_cli`, `windsurf`, `devin`, `opencode`, `antigravity`,
`copilot_cli`, `commandcode`, `qwen_code`, `vscode`, `grok_build`, `omp`, `pi`,
`copilot_cli`, `commandcode`, `qwen_code`, `vscode`, `trae`, `grok_build`, `omp`, `pi`,
`hermes`, `zcode`, `dsh`, or `grok_bot`. If unknown, inspect `doctor --json` before choosing a
scope. `grok_bot` uses a chat registration step rather than local `mcp.json`.
For unsupported clients, use version-pinned manual stdio registration or CLI.

View File

@ -145,6 +145,21 @@ REA follows `HERMES_HOME` whitespace trimming, environment-variable expansion,
and home expansion before resolving that profile. Setup previews the resolved
configuration and skill destinations; doctor and uninstall use those same paths.
## Trae
For [Trae](https://docs.trae.ai/ide/add-mcp-servers), setup writes
`mcpServers.rea` into the user-scope `mcp.json` under Trae's VS Code-style
user directory (`%APPDATA%\Trae\User` on Windows,
`~/Library/Application Support/Trae/User` on macOS, and
`~/.config/Trae/User` elsewhere). Entries carry `command`, `args`, and
`env`; stdio is the default transport. Trae discovers personal skills from
`~/.trae/skills` (`%userprofile%\.trae\skills` on Windows), so setup
installs its skill copy there.
The user-level MCP paths follow the editor's VS Code layout. Trae's official
documentation describes the raw JSON editor without specifying those paths;
loading this registration in Trae remains unverified.
## ZCode
For [ZCode](https://zcode.z.ai/en/docs/mcp-services), setup registers `rea` in the nested `mcp.servers` table of the

View File

@ -280,6 +280,7 @@ hosted connector; see [its configuration](client-configuration.md#other-client-s
| OMP | `omp` |
| Pi | `pi` |
| Hermes | `hermes` |
| Trae | `trae` |
| ZCode | `zcode` |
| DeepSeek Harness | `dsh` |

View File

@ -91,6 +91,22 @@ const vscodeUserDirectory = ({
return join(env.XDG_CONFIG_HOME ?? join(home, ".config"), "Code", "User");
};
const traeUserDirectory = ({
home,
platform,
env,
}: ClientPathContext): string => {
if (platform === "win32")
return join(
env.APPDATA ?? join(home, "AppData", "Roaming"),
"Trae",
"User",
);
if (platform === "darwin")
return join(home, "Library", "Application Support", "Trae", "User");
return join(env.XDG_CONFIG_HOME ?? join(home, ".config"), "Trae", "User");
};
const claudeDesktopDirectory = ({
home,
platform,
@ -464,6 +480,15 @@ export const SUPPORTED_CLIENT_DEFINITIONS = [
markerPath: vscodeUserDirectory,
format: "vscode",
},
{
name: "trae",
displayName: "Trae",
skillPath: [".trae", "skills"],
configPath: (context: ClientPathContext) =>
join(traeUserDirectory(context), "mcp.json"),
markerPath: traeUserDirectory,
format: "json",
},
{
name: "grok_build",
displayName: "Grok Build",

View File

@ -65,6 +65,7 @@ describe("client configuration filesystem lifecycle", () => {
"commandcode",
"qwen_code",
"vscode",
"trae",
"grok_build",
"omp",
"pi",

View File

@ -0,0 +1,173 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js";
import { npxRegistrationCommand } from "../../../src/application/ClientRegistrationIdentity.js";
import { configureClientConfiguration } from "../../../src/application/SetupClientConfiguration.js";
import { skillDestinations } from "../../../src/application/SetupSkill.js";
import { supportedClients } from "../../../src/application/SupportedClients.js";
import { systemUninstallHost } from "../../../src/application/Uninstall.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { clearClientLocationEnvironment } from "../../fixtures/clientEnvironment.js";
/**
* The registration command setup writes on the host running the tests. Windows
* wraps it in `cmd.exe /d /c`, so the expected entry follows the host too.
*/
const command = npxRegistrationCommand(process.platform);
beforeEach(clearClientLocationEnvironment);
afterEach(() => vi.unstubAllEnvs());
const traePath = (
home: string,
platform: NodeJS.Platform = "linux",
env: Parameters<typeof supportedClients>[2] = {},
) => supportedClients(home, platform, env).find(({ name }) => name === "trae");
describe("Trae configuration paths", () => {
it("resolves the VS Code-style user directory per platform", () => {
expect(traePath("/home/a", "linux", {})?.configPath).toBe(
join("/home/a", ".config", "Trae", "User", "mcp.json"),
);
expect(
traePath("/home/a", "linux", { XDG_CONFIG_HOME: "/xdg" })?.configPath,
).toBe(join("/xdg", "Trae", "User", "mcp.json"));
expect(traePath("/Users/a", "darwin")?.configPath).toBe(
join(
"/Users/a",
"Library",
"Application Support",
"Trae",
"User",
"mcp.json",
),
);
expect(
traePath("C:\\Users\\a", "win32", {
APPDATA: "C:\\Users\\a\\AppData\\Roaming",
})?.configPath,
).toBe(join("C:\\Users\\a\\AppData\\Roaming", "Trae", "User", "mcp.json"));
expect(
traePath("C:\\Users\\a", "win32", {
APPDATA: "C:\\Users\\a\\AppData\\Roaming",
})?.markerPath,
).toBe(join("C:\\Users\\a\\AppData\\Roaming", "Trae", "User"));
});
it("resolves the registration format as the plain mcpServers JSON dialect", () => {
expect(traePath("/home/a")?.format).toBe("json");
});
it("resolves the personal skills directory under the home root", () => {
expect(
skillDestinations("/home/a", ["trae"]).map(({ path }) => path),
).toEqual([
join("/home/a", ".trae", "skills", "reverse-engineer-anything"),
]);
expect(
skillDestinations("C:\\Users\\a", ["trae"], {}, "win32").map(
({ path }) => path,
),
).toEqual([
join("C:\\Users\\a", ".trae", "skills", "reverse-engineer-anything"),
]);
});
});
describe("Trae JSON registration", () => {
const traeClient = async () => {
const home = await createTestTempDirectory("rea-trae-");
const client = traePath(home, process.platform);
if (client?.markerPath === undefined)
throw new Error("missing Trae client");
await mkdir(client.markerPath, { recursive: true });
return { home, client };
};
it("upserts rea into mcpServers while preserving unrelated settings", async () => {
const { home, client } = await traeClient();
await writeFile(
client.configPath,
`{
"mcpServers": {
"other": { "command": "other-server" }
}
}`,
);
expect(
(await configureClientConfiguration(client, {}, command)).status,
).toBe("configured");
const parsed = JSON.parse(await readFile(client.configPath, "utf8"));
expect(parsed.mcpServers.other).toEqual({ command: "other-server" });
expect(parsed.mcpServers.rea).toEqual({
command: command[0],
args: command.slice(1),
});
expect(await configureClientConfiguration(client, {}, command)).toEqual({
status: "unchanged",
});
expect(
(
await readClientRegistrationStatuses(home, undefined, {
platform: process.platform,
environment: {},
})
).find(({ client }) => client === "trae"),
).toMatchObject({ state: "aligned", command });
});
it("creates the mcpServers table in an existing mcp.json without servers", async () => {
const { client } = await traeClient();
await writeFile(
client.configPath,
JSON.stringify({ unrelated: { setting: true } }, null, 2),
);
expect(
(await configureClientConfiguration(client, {}, command)).status,
).toBe("configured");
const parsed = JSON.parse(await readFile(client.configPath, "utf8"));
expect(parsed.unrelated).toEqual({ setting: true });
expect(parsed.mcpServers.rea).toEqual({
command: command[0],
args: command.slice(1),
});
});
it("registers into an empty mcp.json", async () => {
const { client } = await traeClient();
await writeFile(client.configPath, "");
expect(
(await configureClientConfiguration(client, {}, command)).status,
).toBe("configured");
const parsed = JSON.parse(await readFile(client.configPath, "utf8"));
expect(parsed.mcpServers.rea).toEqual({
command: command[0],
args: command.slice(1),
});
});
it("removes only the rea entry on uninstall", async () => {
const { home, client } = await traeClient();
await writeFile(
client.configPath,
JSON.stringify(
{ mcpServers: { other: { command: "other-server" } } },
null,
2,
),
);
await configureClientConfiguration(client, {}, command);
expect((await systemUninstallHost(home).removeClient(client)).status).toBe(
"removed",
);
const parsed = JSON.parse(await readFile(client.configPath, "utf8"));
expect(parsed.mcpServers.other).toEqual({ command: "other-server" });
expect(parsed.mcpServers.rea).toBeUndefined();
});
});