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(); + }); +});