From fb841a9596f5195f52f1946a2ee42e109c30cdaf Mon Sep 17 00:00:00 2001 From: Xiangfang Chen <565499699@qq.com> Date: Fri, 6 Nov 2026 15:38:47 +0800 Subject: [PATCH] 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. --- docs/client-configuration.md | 12 ++ docs/installation.md | 1 + src/application/SupportedClients.ts | 8 + .../filesystem/clientLifecycle.test.ts | 1 + .../codebuddyClientConfiguration.test.ts | 148 ++++++++++++++++++ 5 files changed, 170 insertions(+) create mode 100644 tests/boundary/filesystem/codebuddyClientConfiguration.test.ts diff --git a/docs/client-configuration.md b/docs/client-configuration.md index 998719288e..16417ef4d0 100644 --- a/docs/client-configuration.md +++ b/docs/client-configuration.md @@ -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 diff --git a/docs/installation.md b/docs/installation.md index 7144221c29..97d2fcfd8f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -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` | diff --git a/src/application/SupportedClients.ts b/src/application/SupportedClients.ts index 09b44b67af..6bf7e8dbf4 100644 --- a/src/application/SupportedClients.ts +++ b/src/application/SupportedClients.ts @@ -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", diff --git a/tests/boundary/filesystem/clientLifecycle.test.ts b/tests/boundary/filesystem/clientLifecycle.test.ts index 474bf5be61..2a4700e200 100644 --- a/tests/boundary/filesystem/clientLifecycle.test.ts +++ b/tests/boundary/filesystem/clientLifecycle.test.ts @@ -50,6 +50,7 @@ describe("client configuration filesystem lifecycle", () => { "claude_desktop", "codex", "cursor", + "codebuddy", "gemini_cli", "windsurf", "devin", diff --git a/tests/boundary/filesystem/codebuddyClientConfiguration.test.ts b/tests/boundary/filesystem/codebuddyClientConfiguration.test.ts new file mode 100644 index 0000000000..68a5934371 --- /dev/null +++ b/tests/boundary/filesystem/codebuddyClientConfiguration.test.ts @@ -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[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(); + }); +});