feat(setup): register OMP during setup (#1301)

* feat(setup): register OMP during setup

Add OMP (oh-my-pi) as a setup, doctor and uninstall client. Setup writes
a type: stdio entry to the user-level agent mcp.json that OMP reads,
following PI_CONFIG_DIR, an absolute PI_CODING_AGENT_DIR and the
OMP_PROFILE/PI_PROFILE named profile. It removes rea from OMP's
disabledServers denylist, which would otherwise hide the registration,
and doctor reports a listed registration as stale.

Closes #1257

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(setup): honor OMP's enabledServers allowlist in doctor

OMP's user enabledServers allowlist forces an entry with enabled: false
on, unless disabledServers also lists it. Doctor reported such a working
registration as stale. Also format the FAQ client list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(setup): keep OMP's allowlisted entry in update maintenance

rea update skipped an owned OMP entry with enabled: false even when
OMP's enabledServers allowlist runs it. Apply the same forced-enable rule
as doctor. Share the client-location environment reset across the client
tests instead of repeating it in each file.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Kaoru0822
2026-10-09 08:51:12 -07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 24673aaae4
commit 8b4c359135
19 changed files with 412 additions and 61 deletions
+10
View File
@@ -137,6 +137,16 @@ listed after the table because its connector is not one of these files:
| Command Code | `commandcode` |
| VS Code | `vscode` |
| Grok Build | `grok_build` |
| OMP | `omp` |
For OMP, setup writes a `type: "stdio"` entry to the user-level
`~/.omp/agent/mcp.json`. It follows `PI_CONFIG_DIR`, an absolute
`PI_CODING_AGENT_DIR`, and the profile selected by `OMP_PROFILE` or
`PI_PROFILE` (`~/.omp/profiles/<name>/agent/mcp.json`). Setup also removes
`rea` from that file's `disabledServers` list, which would otherwise hide the
registration. Doctor treats an `enabled: false` entry as active when
`enabledServers` lists `rea`, as OMP does, unless `disabledServers` also lists
it. Run setup under each profile that should load REA.
For OpenCode, setup writes the V1 `mcp.rea` entry, which OpenCode V1 and V2
both load. If the configuration already uses OpenCode V2's native
+1 -1
View File
@@ -67,7 +67,7 @@ REA setup lets you select agent integration and optional Hopper installation.
It installs the bundled workflow, configures detected agents, and can save
verified paths for an existing Ghidra installation. It configures Claude Code,
Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity,
GitHub Copilot CLI, Command Code, VS Code, and Grok Build using each client's configuration format. Grok Bot is detected locally, but its connector is added from the Grok Bot chat because the signed-in account store is not a data-directory file.
GitHub Copilot CLI, Command Code, VS Code, Grok Build, and OMP using each client's configuration format. Grok Bot is detected locally, but its connector is added from the Grok Bot chat because the signed-in account store is not a data-directory file.
Ghidra analysis supports Linux x64/arm64 and macOS x64/arm64 with Ghidra 12.1.x and the
64-bit full JDK that installation declares. Current 12.1 releases require JDK 21
+4
View File
@@ -81,6 +81,10 @@ export async function verifyPackageEnvironment({
COPILOT_HOME: join(home, ".copilot"),
GROK_HOME: join(home, ".grok"),
SAND_DATA_ROOT: join(home, ".grokbot"),
OMP_PROFILE: undefined,
PI_CODING_AGENT_DIR: undefined,
PI_CONFIG_DIR: undefined,
PI_PROFILE: undefined,
OPENCODE_CONFIG: undefined,
XDG_CONFIG_HOME: join(home, ".config"),
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
+4
View File
@@ -103,6 +103,10 @@ export async function verifyPackageUpdate({
COPILOT_HOME: join(home, ".copilot"),
GROK_HOME: join(home, ".grok"),
SAND_DATA_ROOT: join(home, ".grokbot"),
OMP_PROFILE: undefined,
PI_CODING_AGENT_DIR: undefined,
PI_CONFIG_DIR: undefined,
PI_PROFILE: undefined,
XDG_CONFIG_HOME: join(home, ".config"),
OPENCODE_CONFIG: join(home, ".config", "opencode", "opencode.json"),
npm_config_prefix: prefix,
+4
View File
@@ -106,6 +106,10 @@ async function verifyPublishedUpdate({ version: targetVersion, canaryRoot }) {
COPILOT_HOME: join(home, ".copilot"),
GROK_HOME: join(home, ".grok"),
SAND_DATA_ROOT: join(home, ".grokbot"),
OMP_PROFILE: undefined,
PI_CODING_AGENT_DIR: undefined,
PI_CONFIG_DIR: undefined,
PI_PROFILE: undefined,
XDG_CONFIG_HOME: join(home, ".config"),
OPENCODE_CONFIG: join(home, ".config", "opencode", "opencode.json"),
npm_config_cache: join(canaryRoot, "update-cache"),
+4
View File
@@ -115,6 +115,10 @@ try {
COPILOT_HOME: join(home, ".copilot"),
GROK_HOME: join(home, ".grok"),
SAND_DATA_ROOT: join(home, ".grokbot"),
OMP_PROFILE: undefined,
PI_CODING_AGENT_DIR: undefined,
PI_CONFIG_DIR: undefined,
PI_PROFILE: undefined,
};
await mkdir(join(home, ".config", "opencode"), { recursive: true });
await writeFile(
+1 -1
View File
@@ -31,7 +31,7 @@ When tools are absent or registration is stale:
`npx -y rea-agents@latest doctor --client codex --json`. Substitute the current
supported client: `claude_code`, `claude_desktop`, `codex`, `cursor`,
`gemini_cli`, `windsurf`, `devin`, `opencode`, `antigravity`, `copilot_cli`,
`commandcode`, `vscode`, `grok_build`, or `grok_bot`. `grok_bot` has no local
`commandcode`, `vscode`, `grok_build`, `omp`, or `grok_bot`. `grok_bot` has no local
file registration: doctor reports the Grok Bot chat step, and setup does not
write `mcp.json`. If the client is unknown, use `doctor --json` and
inspect its registration results before choosing a setup scope.
@@ -1037,6 +1037,47 @@ export const grokServerListedDisabled = (
Array.isArray(document.disabled_mcp_servers) &&
document.disabled_mcp_servers.includes(serverKey);
/**
* OMP's user `disabledServers` denylist hides a server by name regardless of
* its entry, so a listed REA registration is not loaded.
*/
export const OMP_DISABLED_SERVERS_KEY = "disabledServers";
/** Whether OMP's root `disabledServers` denylist names `serverKey`. */
export const ompServerListedDisabled = (
document: Record<string, unknown>,
serverKey: string,
): boolean => {
const listed = document[OMP_DISABLED_SERVERS_KEY];
return Array.isArray(listed) && listed.includes(serverKey);
};
/** OMP's user `enabledServers` allowlist overrides an entry's `enabled: false`. */
const OMP_ENABLED_SERVERS_KEY = "enabledServers";
/**
* Whether OMP's allowlist forces `serverKey` on despite its entry's
* `enabled: false`. The `disabledServers` denylist still wins over it.
*/
export const clientServerForcedEnabled = (
parsed: ClientConfigurationDocument,
serverKey: string,
): boolean => {
if (parsed.dialect !== "omp") return false;
const listed = parsed.document[OMP_ENABLED_SERVERS_KEY];
return Array.isArray(listed) && listed.includes(serverKey);
};
/** Whether a client's own disable list suppresses `serverKey`. */
export const clientServerListedDisabled = (
parsed: ClientConfigurationDocument,
serverKey: string,
): boolean =>
(parsed.dialect === "grok" &&
grokServerListedDisabled(parsed.document, serverKey)) ||
(parsed.dialect === "omp" &&
ompServerListedDisabled(parsed.document, serverKey));
/** Keep an existing Grok document intact aside from the REA server tables. */
const serializeGrokConfiguration = (
document: Record<string, unknown>,
@@ -1289,6 +1330,7 @@ export const clientRegistrationEntry = (
: { environment: { ...environment } }),
};
case "vscode":
case "omp":
return {
type: "stdio",
command: executable,
+26 -9
View File
@@ -1,6 +1,7 @@
import {
clientServerForcedEnabled,
clientServerListedDisabled,
effectiveClientServer,
grokServerListedDisabled,
parseClientConfiguration,
} from "./ClientConfigurationDocument.js";
import { access, readFile } from "node:fs/promises";
@@ -85,7 +86,11 @@ export const readClientRegistrationStatuses = async (
CODEX_HOME: options.environment.CODEX_HOME,
COPILOT_HOME: options.environment.COPILOT_HOME,
GROK_HOME: options.environment.GROK_HOME,
OMP_PROFILE: options.environment.OMP_PROFILE,
OPENCODE_CONFIG: options.environment.OPENCODE_CONFIG,
PI_CODING_AGENT_DIR: options.environment.PI_CODING_AGENT_DIR,
PI_CONFIG_DIR: options.environment.PI_CONFIG_DIR,
PI_PROFILE: options.environment.PI_PROFILE,
SAND_DATA_ROOT: options.environment.SAND_DATA_ROOT,
XDG_CONFIG_HOME: options.environment.XDG_CONFIG_HOME,
},
@@ -132,14 +137,9 @@ export const readClientRegistrationStatuses = async (
client,
currentCommandPath,
options.platform ?? process.platform,
clientServerForcedEnabled(parsed, PRODUCT_IDENTITY.mcpServerKey),
) &&
!(
client.format === "grok" &&
grokServerListedDisabled(
parsed.document,
PRODUCT_IDENTITY.mcpServerKey,
)
)
!clientServerListedDisabled(parsed, PRODUCT_IDENTITY.mcpServerKey)
? "aligned"
: "stale",
),
@@ -164,9 +164,13 @@ const registrationAligned = (
client: SetupClient,
currentCommandPath: string,
platform: NodeJS.Platform,
forcedEnabled = false,
): boolean => {
const command = [registration.command, ...registration.args];
if (registration.disabled === true || registration.enabled === false)
if (
registration.disabled === true ||
(registration.enabled === false && !forcedEnabled)
)
return false;
if (!isOwnedClientRegistrationCommand(command, currentCommandPath))
return false;
@@ -177,6 +181,13 @@ const registrationAligned = (
)
return false;
if (client.format === "vscode" && registration.type !== "stdio") return false;
// OMP infers stdio for a command entry without an explicit type.
if (
client.format === "omp" &&
registration.type !== undefined &&
registration.type !== "stdio"
)
return false;
if (
client.format === "copilot_cli" &&
(registration.type !== "stdio" ||
@@ -233,6 +244,12 @@ const parseRegistration = (
throw new TypeError("Expected an stdio registration");
if (client.format === "commandcode" && registration.transport !== "stdio")
throw new TypeError("Expected an stdio registration");
if (
client.format === "omp" &&
registration.type !== undefined &&
registration.type !== "stdio"
)
throw new TypeError("Expected an stdio registration");
return registration;
};
+23 -7
View File
@@ -1,9 +1,10 @@
import {
clientRegistrationEntry,
clientConfigurationValuesEqual,
clientServerListedDisabled,
clientServerPath,
grokServerListedDisabled,
legacyClientServerPath,
OMP_DISABLED_SERVERS_KEY,
parseClientConfiguration,
serializeClientConfiguration,
withClientServers,
@@ -85,11 +86,28 @@ const configureClientDocument = async (
parsed,
PRODUCT_IDENTITY.mcpServerKey,
);
const document = withClientServers(
const registered = withClientServers(
parsed,
{ ...parsed.servers, [PRODUCT_IDENTITY.mcpServerKey]: desired },
PRODUCT_IDENTITY.mcpServerKey,
);
// OMP's denylist would hide the registration; keep every other listed name.
const ompDisabled =
parsed.dialect === "omp"
? parsed.document[OMP_DISABLED_SERVERS_KEY]
: undefined;
const enableOmp =
Array.isArray(ompDisabled) &&
ompDisabled.includes(PRODUCT_IDENTITY.mcpServerKey);
const document =
enableOmp && Array.isArray(ompDisabled)
? {
...registered,
[OMP_DISABLED_SERVERS_KEY]: ompDisabled.filter(
(name: unknown) => name !== PRODUCT_IDENTITY.mcpServerKey,
),
}
: registered;
try {
await mkdir(dirname(client.configPath), { recursive: true });
await writeFileAtomic(
@@ -97,6 +115,7 @@ const configureClientDocument = async (
serializeClientConfiguration(document, format, original, [
clientServerPath(parsed, PRODUCT_IDENTITY.mcpServerKey),
...(legacyPath === undefined ? [] : [legacyPath]),
...(enableOmp ? [[OMP_DISABLED_SERVERS_KEY]] : []),
]),
{
encoding: "utf8",
@@ -233,7 +252,7 @@ const restoreConfig = async (
}
};
/** Whether REA's entry matches, no legacy entry remains, and Grok is not suppressing it. */
/** Whether REA's entry matches, no legacy entry remains, and no client disable list suppresses it. */
const registrationCurrent = (
parsed: ClientConfigurationDocument,
desired: unknown,
@@ -243,10 +262,7 @@ const registrationCurrent = (
desired,
) &&
!Object.hasOwn(parsed.legacyServers, PRODUCT_IDENTITY.mcpServerKey) &&
!(
parsed.dialect === "grok" &&
grokServerListedDisabled(parsed.document, PRODUCT_IDENTITY.mcpServerKey)
);
!clientServerListedDisabled(parsed, PRODUCT_IDENTITY.mcpServerKey);
const clientConfigurationDesired = (
client: SetupClient,
+51
View File
@@ -15,6 +15,7 @@ export interface SetupClient {
| "opencode"
| "commandcode"
| "grok"
| "omp"
| "unsupported";
}
@@ -29,7 +30,11 @@ interface ClientPathContext {
readonly CODEX_HOME?: string | undefined;
readonly COPILOT_HOME?: string | undefined;
readonly GROK_HOME?: string | undefined;
readonly OMP_PROFILE?: string | undefined;
readonly OPENCODE_CONFIG?: string | undefined;
readonly PI_CODING_AGENT_DIR?: string | undefined;
readonly PI_CONFIG_DIR?: string | undefined;
readonly PI_PROFILE?: string | undefined;
readonly SAND_DATA_ROOT?: string | undefined;
readonly XDG_CONFIG_HOME?: string | undefined;
};
@@ -106,6 +111,40 @@ export const manualRegistrationRemediation = (
? GROK_BOT_MANUAL_REGISTRATION_REMEDIATION
: undefined;
const OMP_PROFILE_NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
/**
* OMP's active named profile. OMP_PROFILE wins over the legacy PI_PROFILE even
* when empty; empty and "default" select the default profile. OMP refuses to
* start with an invalid name, so one leaves the default location in place.
*/
const ompProfile = ({ env }: ClientPathContext): string | undefined => {
const name = (env.OMP_PROFILE ?? env.PI_PROFILE)?.trim();
return name === undefined ||
name === "" ||
name === "default" ||
name.endsWith(".") ||
!OMP_PROFILE_NAME.test(name)
? undefined
: name;
};
/**
* The OMP agent directory whose `mcp.json` holds user-scope MCP servers. OMP
* joins PI_CONFIG_DIR to the home directory, and a named profile ignores
* PI_CODING_AGENT_DIR. OMP resolves a relative agent directory against each
* process's working directory, so only an absolute override names one file.
*/
const ompAgentDirectory = (context: ClientPathContext): string => {
const root = join(context.home, context.env.PI_CONFIG_DIR || ".omp");
const profile = ompProfile(context);
if (profile !== undefined) return join(root, "profiles", profile, "agent");
const override = context.env.PI_CODING_AGENT_DIR;
return override !== undefined && override !== "" && isAbsolute(override)
? override
: join(root, "agent");
};
const copilotDirectory = ({ home, env }: ClientPathContext): string =>
env.COPILOT_HOME ?? join(home, ".copilot");
@@ -249,6 +288,14 @@ export const SUPPORTED_CLIENT_DEFINITIONS = [
markerPath: grokDirectory,
format: "grok",
},
{
name: "omp",
displayName: "OMP",
configPath: (context: ClientPathContext) =>
join(ompAgentDirectory(context), "mcp.json"),
markerPath: ompAgentDirectory,
format: "omp",
},
{
name: "grok_bot",
displayName: "Grok Bot",
@@ -271,7 +318,11 @@ export const supportedClients = (
CODEX_HOME: process.env.CODEX_HOME,
COPILOT_HOME: process.env.COPILOT_HOME,
GROK_HOME: process.env.GROK_HOME,
OMP_PROFILE: process.env.OMP_PROFILE,
OPENCODE_CONFIG: process.env.OPENCODE_CONFIG,
PI_CODING_AGENT_DIR: process.env.PI_CODING_AGENT_DIR,
PI_CONFIG_DIR: process.env.PI_CONFIG_DIR,
PI_PROFILE: process.env.PI_PROFILE,
SAND_DATA_ROOT: process.env.SAND_DATA_ROOT,
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
},
+7 -1
View File
@@ -6,6 +6,7 @@ import { PRODUCT_IDENTITY } from "../identity.js";
import { isOwnedClientRegistrationCommand } from "./ClientRegistrationIdentity.js";
import { readClientRegistrationStatuses } from "./ClientRegistrationStatus.js";
import {
clientServerForcedEnabled,
effectiveClientServer,
parseClientConfiguration,
} from "./ClientConfigurationDocument.js";
@@ -76,7 +77,12 @@ export const existingMaintenanceScope = async (
disabled: z.boolean().optional(),
})
.parse(effectiveClientServer(parsed, PRODUCT_IDENTITY.mcpServerKey));
if (enabled.enabled !== false && enabled.disabled !== true)
// OMP's enabledServers allowlist runs an entry marked `enabled: false`.
if (
(enabled.enabled !== false ||
clientServerForcedEnabled(parsed, PRODUCT_IDENTITY.mcpServerKey)) &&
enabled.disabled !== true
)
clients.push(client.name);
}
let skill = false;
@@ -11,6 +11,7 @@ import { supportedClients } from "../../../src/application/SupportedClients.js";
import { PRODUCT_IDENTITY } from "../../../src/identity.js";
import { isOwnedClientRegistrationCommand } from "../../../src/application/ClientRegistrationIdentity.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { clearClientLocationEnvironment } from "../../fixtures/clientEnvironment.js";
const command = [
"npx",
@@ -19,19 +20,7 @@ const command = [
"mcp",
] as const;
beforeEach(() => {
for (const name of [
"APPDATA",
"CLAUDE_CONFIG_DIR",
"CODEX_HOME",
"COPILOT_HOME",
"GROK_HOME",
"OPENCODE_CONFIG",
"SAND_DATA_ROOT",
"XDG_CONFIG_HOME",
])
vi.stubEnv(name, undefined);
});
beforeEach(clearClientLocationEnvironment);
afterEach(() => vi.unstubAllEnvs());
@@ -44,6 +33,7 @@ const getClients = (home: string) =>
"commandcode",
"vscode",
"devin",
"omp",
].includes(name),
);
@@ -55,6 +45,7 @@ describe("additional client configuration dialects", () => {
"commandcode",
"vscode",
"devin",
"omp",
] as const)("registers, reads back, and uninstalls %s", async (name) => {
const home = await createTestTempDirectory("rea-client-dialect-");
const client = getClients(home).find(
@@ -89,7 +80,7 @@ describe("additional client configuration dialects", () => {
command,
enabled: true,
});
if (name === "vscode")
if (name === "vscode" || name === "omp")
expect(registration).toMatchObject({
type: "stdio",
command: "npx",
@@ -15,6 +15,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { parse as parseJsonc, type ParseError } from "jsonc-parser";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { clearClientLocationEnvironment } from "../../fixtures/clientEnvironment.js";
import { supportedClients } from "../../../src/application/SupportedClients.js";
import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js";
@@ -30,19 +31,7 @@ import {
} from "../../../src/application/Uninstall.js";
const roots: string[] = [];
beforeEach(() => {
for (const name of [
"APPDATA",
"CLAUDE_CONFIG_DIR",
"CODEX_HOME",
"COPILOT_HOME",
"GROK_HOME",
"OPENCODE_CONFIG",
"SAND_DATA_ROOT",
"XDG_CONFIG_HOME",
])
vi.stubEnv(name, undefined);
});
beforeEach(clearClientLocationEnvironment);
afterEach(async () => {
vi.unstubAllEnvs();
await Promise.all(
@@ -72,6 +61,7 @@ describe("client configuration filesystem lifecycle", () => {
"commandcode",
"vscode",
"grok_build",
"omp",
"grok_bot",
]);
expect(
@@ -4,23 +4,12 @@ import { join, resolve } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { clearClientLocationEnvironment } from "../../fixtures/clientEnvironment.js";
import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js";
import { PRODUCT_IDENTITY } from "../../../src/identity.js";
beforeEach(() => {
for (const name of [
"APPDATA",
"CLAUDE_CONFIG_DIR",
"CODEX_HOME",
"COPILOT_HOME",
"GROK_HOME",
"OPENCODE_CONFIG",
"SAND_DATA_ROOT",
"XDG_CONFIG_HOME",
])
vi.stubEnv(name, undefined);
});
beforeEach(clearClientLocationEnvironment);
afterEach(() => {
vi.unstubAllEnvs();
});
@@ -0,0 +1,174 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { parse as parseJsonc } from "jsonc-parser";
import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js";
import { configureClientConfiguration } from "../../../src/application/SetupClientConfiguration.js";
import { supportedClients } from "../../../src/application/SupportedClients.js";
import { systemUninstallHost } from "../../../src/application/Uninstall.js";
import { PRODUCT_IDENTITY } from "../../../src/identity.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { clearClientLocationEnvironment } from "../../fixtures/clientEnvironment.js";
const command = [
"npx",
"-y",
PRODUCT_IDENTITY.registrationPackageSpecifier,
"mcp",
] as const;
beforeEach(clearClientLocationEnvironment);
afterEach(() => vi.unstubAllEnvs());
const ompPath = (env: Parameters<typeof supportedClients>[2]) =>
supportedClients("/home/a", "linux", env).find(({ name }) => name === "omp");
describe("OMP configuration paths", () => {
it("uses the default agent directory and OMP's environment overrides", () => {
expect(ompPath({})).toEqual({
name: "omp",
displayName: "OMP",
configPath: "/home/a/.omp/agent/mcp.json",
markerPath: "/home/a/.omp/agent",
format: "omp",
});
// OMP joins PI_CONFIG_DIR to the home directory.
expect(ompPath({ PI_CONFIG_DIR: ".pi" })?.configPath).toBe(
"/home/a/.pi/agent/mcp.json",
);
expect(ompPath({ PI_CODING_AGENT_DIR: "/custom/agent" })?.configPath).toBe(
"/custom/agent/mcp.json",
);
// A relative override names a different file in every working directory.
expect(ompPath({ PI_CODING_AGENT_DIR: "agent" })?.configPath).toBe(
"/home/a/.omp/agent/mcp.json",
);
});
it("follows the active named profile like OMP", () => {
expect(
ompPath({ OMP_PROFILE: "work", PI_CODING_AGENT_DIR: "/custom/agent" })
?.configPath,
).toBe("/home/a/.omp/profiles/work/agent/mcp.json");
expect(ompPath({ PI_PROFILE: "legacy" })?.configPath).toBe(
"/home/a/.omp/profiles/legacy/agent/mcp.json",
);
// An explicitly empty OMP_PROFILE selects the default over PI_PROFILE.
expect(ompPath({ OMP_PROFILE: "", PI_PROFILE: "legacy" })?.configPath).toBe(
"/home/a/.omp/agent/mcp.json",
);
for (const profile of ["default", " ", "Bad Name", "trailing."])
expect(ompPath({ OMP_PROFILE: profile })?.configPath).toBe(
"/home/a/.omp/agent/mcp.json",
);
});
});
describe("OMP disabledServers denylist", () => {
const ompClient = async () => {
const home = await createTestTempDirectory("rea-omp-");
const client = supportedClients(home, process.platform, {}).find(
({ name }) => name === "omp",
);
if (client?.markerPath === undefined) throw new Error("missing OMP client");
await mkdir(client.markerPath, { recursive: true });
return { home, client };
};
it("removes rea from disabledServers while keeping other names and comments", async () => {
const { home, client } = await ompClient();
await writeFile(
client.configPath,
`{
// Keep this note.
"mcpServers": { "other": { "command": "other-server" } },
"disabledServers": ["other", "rea"]
}
`,
);
expect(
(await configureClientConfiguration(client, {}, command)).status,
).toBe("configured");
const text = await readFile(client.configPath, "utf8");
expect(text).toContain("// Keep this note.");
expect(parseJsonc(text)).toEqual({
mcpServers: {
other: { command: "other-server" },
rea: { type: "stdio", command: "npx", args: command.slice(1) },
},
disabledServers: ["other"],
});
expect(await configureClientConfiguration(client, {}, command)).toEqual({
status: "unchanged",
});
expect((await systemUninstallHost(home).removeClient(client)).status).toBe(
"removed",
);
expect(parseJsonc(await readFile(client.configPath, "utf8"))).toEqual({
mcpServers: { other: { command: "other-server" } },
disabledServers: ["other"],
});
});
it("reports a registration hidden by disabledServers as stale", async () => {
const { home, client } = await ompClient();
const entry = resolve("scripts/rea.mjs");
const registration = {
command: process.execPath,
args: [entry, "mcp"],
};
await writeFile(
client.configPath,
JSON.stringify({
mcpServers: { rea: registration },
disabledServers: ["rea"],
}),
);
const status = async () =>
(
await readClientRegistrationStatuses(home, entry, { environment: {} })
).find(({ client: name }) => name === "omp");
expect(await status()).toMatchObject({ state: "stale" });
// The enabledServers allowlist forces an `enabled: false` entry on, but
// the disabledServers denylist still wins over it.
const disabledEntry = { ...registration, enabled: false };
await writeFile(
client.configPath,
JSON.stringify({ mcpServers: { rea: disabledEntry } }),
);
expect(await status()).toMatchObject({ state: "stale" });
await writeFile(
client.configPath,
JSON.stringify({
mcpServers: { rea: disabledEntry },
enabledServers: ["rea"],
}),
);
expect(await status()).toMatchObject({ state: "aligned" });
await writeFile(
client.configPath,
JSON.stringify({
mcpServers: { rea: disabledEntry },
enabledServers: ["rea"],
disabledServers: ["rea"],
}),
);
expect(await status()).toMatchObject({ state: "stale" });
// OMP infers stdio for a command entry without an explicit type.
await writeFile(
client.configPath,
JSON.stringify({ mcpServers: { rea: registration } }),
);
expect(await status()).toMatchObject({ state: "aligned" });
await writeFile(
client.configPath,
JSON.stringify({
mcpServers: { rea: { ...registration, type: "http" } },
}),
);
expect(await status()).toMatchObject({ state: "invalid" });
expect(join(home, ".omp", "agent", "mcp.json")).toBe(client.configPath);
});
});
+22
View File
@@ -0,0 +1,22 @@
import { vi } from "vitest";
/** Environment variables that redirect where supported clients keep config. */
export const CLIENT_LOCATION_ENVIRONMENT = [
"APPDATA",
"CLAUDE_CONFIG_DIR",
"CODEX_HOME",
"COPILOT_HOME",
"GROK_HOME",
"OMP_PROFILE",
"OPENCODE_CONFIG",
"PI_CODING_AGENT_DIR",
"PI_CONFIG_DIR",
"PI_PROFILE",
"SAND_DATA_ROOT",
"XDG_CONFIG_HOME",
] as const;
/** Clear the developer's client locations so tests resolve only their temporary homes. */
export const clearClientLocationEnvironment = (): void => {
for (const name of CLIENT_LOCATION_ENVIRONMENT) vi.stubEnv(name, undefined);
};
@@ -31,6 +31,10 @@ beforeEach(async () => {
SAND_DATA_ROOT: join(home, ".grokbot"),
XDG_CONFIG_HOME: join(home, ".config"),
OPENCODE_CONFIG: join(home, ".config", "opencode", "opencode.jsonc"),
OMP_PROFILE: undefined,
PI_CODING_AGENT_DIR: undefined,
PI_CONFIG_DIR: undefined,
PI_PROFILE: undefined,
}))
vi.stubEnv(name, value);
});
@@ -75,6 +79,29 @@ const action = {
commands: ["rea mcp"],
};
describe("OMP integration maintenance", () => {
it("refreshes an OMP entry that enabledServers forces on", async () => {
const entry = { type: "stdio", command: entryPoint, args: ["mcp"] };
await writeClient(
"omp",
JSON.stringify({ mcpServers: { rea: { ...entry, enabled: false } } }),
);
await expect(
existingMaintenanceScope(home, entryPoint, process.env),
).resolves.toMatchObject({ clients: [] });
await writeClient(
"omp",
JSON.stringify({
mcpServers: { rea: { ...entry, enabled: false } },
enabledServers: ["rea"],
}),
);
await expect(
existingMaintenanceScope(home, entryPoint, process.env),
).resolves.toMatchObject({ clients: ["omp"] });
});
});
describe("existing REA integration maintenance", () => {
it("selects owned registrations while preserving unconfigured, foreign, and disabled choices", async () => {
await writeClient("codex", staleCodex);
+2 -2
View File
@@ -168,8 +168,8 @@
Any agent that supports local MCP servers. Setup includes
Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI,
Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI,
Command Code, Grok Build, and VS Code. Grok Bot's connector is
added from its chat.
Command Code, Grok Build, OMP, and VS Code. Grok Bot's
connector is added from its chat.
</p>
<p>
<a href="../get-started/#agent">Connect your coding agent</a>.