feat(setup): support CodeBuddy as a setup client (#2492)

Register rea in the global ~/.codebuddy/mcp.json through a new codebuddy
entry in SUPPORTED_CLIENT_DEFINITIONS (json dialect, ~/.codebuddy marker,
~/.codebuddy/skills skill directory). Document the client and cover
registration and uninstall with boundary tests. Loading, the bundled
skill, and real REA tool calls were verified in the CodeBuddy client.
This commit is contained in:
Xiangfang Chen authored and GitHub committed 2026-11-06 15:38:47 +08:00
1 parent 1902fcb13d
commit fb841a9596
5 files changed
+170

No files matched your search

+12
View File
@@ -663,6 +663,18 @@ 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.
## CodeBuddy
For CodeBuddy, setup writes `mcpServers.rea` into the global
`~/.codebuddy/mcp.json` (a standard `mcpServers` JSON file at the user home
root on every platform). Entries carry `command`, `args`, and `env`; stdio is
the default transport. CodeBuddy discovers personal skills from
`~/.codebuddy/skills`, so setup installs its skill copy there.
The global MCP path is a plain JSON file; setup preserves every other server
and top-level key when it upserts `rea`. Loading this registration, the
bundled skill, and real REA tool calls through CodeBuddy are verified.
## WorkBuddy
For [WorkBuddy](https://www.workbuddy.cn/docs/workbuddy/From-Beginner-to-Expert-Guide/Function-Description/MCP-Guide), setup writes `mcpServers.rea` into the
+1
View File
@@ -636,6 +636,7 @@ hosted connector; see [its configuration](client-configuration.md#other-client-s
| Claude Desktop | `claude_desktop` |
| Codex | `codex` |
| Cursor | `cursor` |
| CodeBuddy | `codebuddy` |
| Gemini CLI | `gemini_cli` |
| Windsurf | `windsurf` |
| Devin | `devin` |
+8
View File
@@ -588,6 +588,14 @@ export const SUPPORTED_CLIENT_DEFINITIONS = [
markerPath: [".cursor"],
format: "json",
},
{
name: "codebuddy",
displayName: "CodeBuddy",
skillPath: [".codebuddy", "skills"],
configPath: [".codebuddy", "mcp.json"],
markerPath: [".codebuddy"],
format: "json",
},
{
name: "gemini_cli",
displayName: "Gemini CLI",
@@ -50,6 +50,7 @@ describe("client configuration filesystem lifecycle", () => {
"claude_desktop",
"codex",
"cursor",
"codebuddy",
"gemini_cli",
"windsurf",
"devin",
@@ -0,0 +1,148 @@
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";
const command = npxRegistrationCommand(process.platform);
beforeEach(clearClientLocationEnvironment);
afterEach(() => vi.unstubAllEnvs());
const codebuddyPath = (
home: string,
platform: NodeJS.Platform = process.platform,
env: Parameters<typeof supportedClients>[2] = {},
) =>
supportedClients(home, platform, env).find(
({ name }) => name === "codebuddy",
);
describe("CodeBuddy configuration paths", () => {
it("resolves the global mcp.json under the home root", () => {
expect(codebuddyPath("/home/a")?.configPath).toBe(
join("/home/a", ".codebuddy", "mcp.json"),
);
expect(codebuddyPath("/Users/a")?.markerPath).toBe(
join("/Users/a", ".codebuddy"),
);
expect(codebuddyPath("C:\\Users\\a", "win32")?.configPath).toBe(
join("C:\\Users\\a", ".codebuddy", "mcp.json"),
);
});
it("resolves the registration format as the plain mcpServers JSON dialect", () => {
expect(codebuddyPath("/home/a")?.format).toBe("json");
});
it("resolves the personal skills directory under the home root", () => {
expect(
skillDestinations("/home/a", ["codebuddy"]).map(({ path }) => path),
).toEqual([
join("/home/a", ".codebuddy", "skills", "reverse-engineer-anything"),
]);
});
});
describe("CodeBuddy JSON registration", () => {
const codebuddyClient = async () => {
const home = await createTestTempDirectory("rea-codebuddy-");
const client = codebuddyPath(home, process.platform);
if (client?.markerPath === undefined)
throw new Error("missing CodeBuddy client");
await mkdir(client.markerPath, { recursive: true });
return { home, client };
};
it("upserts rea into mcpServers while preserving unrelated settings", async () => {
const { home, client } = await codebuddyClient();
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 === "codebuddy"),
).toMatchObject({ state: "aligned", command });
});
it("creates the mcpServers table in an existing mcp.json without servers", async () => {
const { client } = await codebuddyClient();
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 codebuddyClient();
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 codebuddyClient();
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();
});
});