From 783f0fe45eb99a1d8af6a053aa09dbe98689dbc1 Mon Sep 17 00:00:00 2001 From: 0xLLVEL Date: Tue, 13 Oct 2026 16:47:47 +0700 Subject: [PATCH] fix(freebuff): detect FreeBuff from its profile directory, not shared .agents The ~/.agents marker made FreeBuff appear installed for any home where REA had installed shared skills (the default skill destination, also Gemini CLI's), producing false doctor failures. Detect FreeBuff from its own fixed profile directory ~/.config/freebuff-desktop instead, and add a regression test covering a home with only the shared agents skill directory. --- docs/client-configuration.md | 3 ++ src/application/SupportedClients.ts | 22 +++++++----- .../freebuffClientConfiguration.test.ts | 36 +++++++++++++------ 3 files changed, 43 insertions(+), 18 deletions(-) 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 }; };