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:
parent
51d6c87cdb
commit
bfa805decf
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ describe("client configuration filesystem lifecycle", () => {
|
|||
"commandcode",
|
||||
"qwen_code",
|
||||
"vscode",
|
||||
"trae",
|
||||
"grok_build",
|
||||
"omp",
|
||||
"pi",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue