diff --git a/docs/client-configuration.md b/docs/client-configuration.md index 66e7a381b2..5d7f48321c 100644 --- a/docs/client-configuration.md +++ b/docs/client-configuration.md @@ -318,6 +318,9 @@ For FreeBuff, setup writes `mcpServers.rea` into the user-scope `~/.agents/skills`, so setup installs its skill copy there. Because the same file doubles as ZCode's user-scope fallback (see above), a `freebuff` registration also loads in ZCode until ZCode defines its own native server. +Setup and doctor detect FreeBuff from its own profile directory +(`~/.config/freebuff-desktop`), not from the shared `.agents` folder that +REA's skill installs create for every client. FreeBuff is main-only and is not part of npm 6.4.0; `rea setup --client freebuff` against `@latest` reports an unsupported client until a release diff --git a/src/application/SupportedClients.ts b/src/application/SupportedClients.ts index c552ade926..828c007c32 100644 --- a/src/application/SupportedClients.ts +++ b/src/application/SupportedClients.ts @@ -481,9 +481,14 @@ const devinDirectory = ({ home, platform, env }: ClientPathContext): string => ? join(env.APPDATA ?? join(home, "AppData", "Roaming"), "devin") : join(env.XDG_CONFIG_HOME || join(home, ".config"), "devin"); -/** FreeBuff loads MCP servers and skills from the shared user agents directory. */ -const freebuffDirectory = ({ home }: ClientPathContext): string => - join(home, ".agents"); +/** + * FreeBuff's own profile directory (its `state.json` root, fixed under + * `.config` on every platform). The shared `~/.agents` folder is not + * FreeBuff-specific: REA's own skill installs create it, so it cannot + * prove FreeBuff is installed. + */ +const freebuffProfileDirectory = ({ home }: ClientPathContext): string => + join(home, ".config", "freebuff-desktop"); /** Stable product metadata used to derive setup discovery and documentation. */ export const SUPPORTED_CLIENT_DEFINITIONS = [ @@ -702,11 +707,12 @@ export const SUPPORTED_CLIENT_DEFINITIONS = [ { name: "freebuff", displayName: "FreeBuff", - skillPath: (context: ClientPathContext) => - join(freebuffDirectory(context), "skills"), - configPath: (context: ClientPathContext) => - join(freebuffDirectory(context), "mcp.json"), - markerPath: freebuffDirectory, + // FreeBuff discovers personal skills in the shared user agents directory. + skillPath: ({ home }: ClientPathContext) => join(home, ".agents", "skills"), + // FreeBuff loads MCP servers from the shared user agents directory. + configPath: ({ home }: ClientPathContext) => + join(home, ".agents", "mcp.json"), + markerPath: freebuffProfileDirectory, format: "json", }, { diff --git a/tests/boundary/filesystem/freebuffClientConfiguration.test.ts b/tests/boundary/filesystem/freebuffClientConfiguration.test.ts index c27244c272..92a94dfc7d 100644 --- a/tests/boundary/filesystem/freebuffClientConfiguration.test.ts +++ b/tests/boundary/filesystem/freebuffClientConfiguration.test.ts @@ -1,11 +1,12 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { join } from "node:path"; +import { dirname, 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 { detectClients } from "../../../src/application/SetupHost.js"; import { skillDestinations } from "../../../src/application/SetupSkill.js"; import { supportedClients } from "../../../src/application/SupportedClients.js"; import { systemUninstallHost } from "../../../src/application/Uninstall.js"; @@ -29,19 +30,35 @@ const freebuffPath = ( supportedClients(home, platform, env).find(({ name }) => name === "freebuff"); describe("FreeBuff configuration paths", () => { - it("resolves the shared user agents file and marker directory", () => { + it("resolves the shared user agents file and the profile marker directory", () => { expect(freebuffPath("/home/a")).toEqual({ name: "freebuff", displayName: "FreeBuff", configPath: join("/home/a", ".agents", "mcp.json"), - markerPath: join("/home/a", ".agents"), + markerPath: join("/home/a", ".config", "freebuff-desktop"), format: "json", }); - expect(freebuffPath("/Users/a", "darwin")?.configPath).toBe( - join("/Users/a", ".agents", "mcp.json"), + expect(freebuffPath("/Users/a", "darwin")?.markerPath).toBe( + join("/Users/a", ".config", "freebuff-desktop"), ); - expect(freebuffPath("C:\\Users\\a", "win32")?.configPath).toBe( - join("C:\\Users\\a", ".agents", "mcp.json"), + expect(freebuffPath("C:\\Users\\a", "win32")?.markerPath).toBe( + join("C:\\Users\\a", ".config", "freebuff-desktop"), + ); + }); + + it("stays undetected when only the shared agents skill directory exists", async () => { + const home = await createTestTempDirectory("rea-freebuff-shared-"); + // REA's own skill installs create this directory for every client. + await mkdir(join(home, ".agents", "skills"), { recursive: true }); + expect((await detectClients(home)).map(({ name }) => name)).not.toContain( + "freebuff", + ); + + await mkdir(join(home, ".config", "freebuff-desktop"), { + recursive: true, + }); + expect((await detectClients(home)).map(({ name }) => name)).toContain( + "freebuff", ); }); @@ -66,9 +83,8 @@ describe("FreeBuff JSON registration", () => { const freebuffClient = async () => { const home = await createTestTempDirectory("rea-freebuff-"); const client = freebuffPath(home, process.platform); - if (client?.markerPath === undefined) - throw new Error("missing FreeBuff client"); - await mkdir(client.markerPath, { recursive: true }); + if (client === undefined) throw new Error("missing FreeBuff client"); + await mkdir(dirname(client.configPath), { recursive: true }); return { home, client }; };