From bfa805decf83744bd3ecb0d1402667cf3ed5c3b2 Mon Sep 17 00:00:00 2001 From: Iven Koo <73214171+IvenKooLab@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:53:30 +0800 Subject: [PATCH] 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> --- .../references/connection-and-recovery.md | 2 +- docs/client-configuration.md | 15 ++ docs/installation.md | 1 + src/application/SupportedClients.ts | 25 +++ .../filesystem/clientLifecycle.test.ts | 1 + .../traeClientConfiguration.test.ts | 173 ++++++++++++++++++ 6 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 tests/boundary/filesystem/traeClientConfiguration.test.ts diff --git a/.agents/skills/reverse-engineer-anything/references/connection-and-recovery.md b/.agents/skills/reverse-engineer-anything/references/connection-and-recovery.md index dbddb0eb9..ae8cd4a98 100644 --- a/.agents/skills/reverse-engineer-anything/references/connection-and-recovery.md +++ b/.agents/skills/reverse-engineer-anything/references/connection-and-recovery.md @@ -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. diff --git a/docs/client-configuration.md b/docs/client-configuration.md index 16ba81400..01bde02a4 100644 --- a/docs/client-configuration.md +++ b/docs/client-configuration.md @@ -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 diff --git a/docs/installation.md b/docs/installation.md index 69e800f05..6b55e316a 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -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` | diff --git a/src/application/SupportedClients.ts b/src/application/SupportedClients.ts index 5c74bb972..3969aa848 100644 --- a/src/application/SupportedClients.ts +++ b/src/application/SupportedClients.ts @@ -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", diff --git a/tests/boundary/filesystem/clientLifecycle.test.ts b/tests/boundary/filesystem/clientLifecycle.test.ts index caf154949..05efa0bfb 100644 --- a/tests/boundary/filesystem/clientLifecycle.test.ts +++ b/tests/boundary/filesystem/clientLifecycle.test.ts @@ -65,6 +65,7 @@ describe("client configuration filesystem lifecycle", () => { "commandcode", "qwen_code", "vscode", + "trae", "grok_build", "omp", "pi", diff --git a/tests/boundary/filesystem/traeClientConfiguration.test.ts b/tests/boundary/filesystem/traeClientConfiguration.test.ts new file mode 100644 index 000000000..202220e52 --- /dev/null +++ b/tests/boundary/filesystem/traeClientConfiguration.test.ts @@ -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[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(); + }); +});