From 8b4c3591358ee59297d8298ede1b63d08459d817 Mon Sep 17 00:00:00 2001
From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Date: Fri, 9 Oct 2026 08:51:12 -0700
Subject: [PATCH] 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
* 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
* 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
---------
Co-authored-by: Claude Opus 5.5
---
docs/installation.md | 10 +
docs/roadmap.md | 2 +-
scripts/verify-package-environment.mjs | 4 +
scripts/verify-package-update.mjs | 4 +
scripts/verify-published-package.mjs | 4 +
scripts/verify-windows-package.mjs | 4 +
skill-src/reverse-engineer-anything/SKILL.md | 2 +-
.../ClientConfigurationDocument.ts | 42 +++++
src/application/ClientRegistrationStatus.ts | 35 +++-
src/application/SetupClientConfiguration.ts | 30 ++-
src/application/SupportedClients.ts | 51 +++++
src/application/UpdateMaintenance.ts | 8 +-
.../clientConfigurationDialects.test.ts | 19 +-
.../filesystem/clientLifecycle.test.ts | 16 +-
.../clientRegistrationStatus.test.ts | 15 +-
.../filesystem/ompClientConfiguration.test.ts | 174 ++++++++++++++++++
tests/fixtures/clientEnvironment.ts | 22 +++
.../process-global/updateMaintenance.test.ts | 27 +++
website/public/faq/index.html | 4 +-
19 files changed, 412 insertions(+), 61 deletions(-)
create mode 100644 tests/boundary/filesystem/ompClientConfiguration.test.ts
create mode 100644 tests/fixtures/clientEnvironment.ts
diff --git a/docs/installation.md b/docs/installation.md
index a0be329205..fad9d1ed0d 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -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//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
diff --git a/docs/roadmap.md b/docs/roadmap.md
index b4ac0ba545..0df3a67c03 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -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
diff --git a/scripts/verify-package-environment.mjs b/scripts/verify-package-environment.mjs
index 90955d5932..f9ee010af8 100644
--- a/scripts/verify-package-environment.mjs
+++ b/scripts/verify-package-environment.mjs
@@ -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 ?? ""}`,
diff --git a/scripts/verify-package-update.mjs b/scripts/verify-package-update.mjs
index fcc27b0914..5998ab77f0 100644
--- a/scripts/verify-package-update.mjs
+++ b/scripts/verify-package-update.mjs
@@ -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,
diff --git a/scripts/verify-published-package.mjs b/scripts/verify-published-package.mjs
index 07c28d76bd..12859d4bb9 100644
--- a/scripts/verify-published-package.mjs
+++ b/scripts/verify-published-package.mjs
@@ -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"),
diff --git a/scripts/verify-windows-package.mjs b/scripts/verify-windows-package.mjs
index 8ece0dde37..67dee06c2f 100644
--- a/scripts/verify-windows-package.mjs
+++ b/scripts/verify-windows-package.mjs
@@ -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(
diff --git a/skill-src/reverse-engineer-anything/SKILL.md b/skill-src/reverse-engineer-anything/SKILL.md
index c2a16cb4fa..d1650f26e8 100644
--- a/skill-src/reverse-engineer-anything/SKILL.md
+++ b/skill-src/reverse-engineer-anything/SKILL.md
@@ -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.
diff --git a/src/application/ClientConfigurationDocument.ts b/src/application/ClientConfigurationDocument.ts
index 038243d8e5..5ae6ad98cc 100644
--- a/src/application/ClientConfigurationDocument.ts
+++ b/src/application/ClientConfigurationDocument.ts
@@ -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,
+ 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,
@@ -1289,6 +1330,7 @@ export const clientRegistrationEntry = (
: { environment: { ...environment } }),
};
case "vscode":
+ case "omp":
return {
type: "stdio",
command: executable,
diff --git a/src/application/ClientRegistrationStatus.ts b/src/application/ClientRegistrationStatus.ts
index 8c2194ac8c..a657f1df4f 100644
--- a/src/application/ClientRegistrationStatus.ts
+++ b/src/application/ClientRegistrationStatus.ts
@@ -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;
};
diff --git a/src/application/SetupClientConfiguration.ts b/src/application/SetupClientConfiguration.ts
index f88a1b5bf8..9519808962 100644
--- a/src/application/SetupClientConfiguration.ts
+++ b/src/application/SetupClientConfiguration.ts
@@ -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,
diff --git a/src/application/SupportedClients.ts b/src/application/SupportedClients.ts
index 7db4cd4256..97467a227c 100644
--- a/src/application/SupportedClients.ts
+++ b/src/application/SupportedClients.ts
@@ -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,
},
diff --git a/src/application/UpdateMaintenance.ts b/src/application/UpdateMaintenance.ts
index 7c6748dd40..bc12c9a9ac 100644
--- a/src/application/UpdateMaintenance.ts
+++ b/src/application/UpdateMaintenance.ts
@@ -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;
diff --git a/tests/boundary/filesystem/clientConfigurationDialects.test.ts b/tests/boundary/filesystem/clientConfigurationDialects.test.ts
index afea88e6f2..609291932c 100644
--- a/tests/boundary/filesystem/clientConfigurationDialects.test.ts
+++ b/tests/boundary/filesystem/clientConfigurationDialects.test.ts
@@ -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",
diff --git a/tests/boundary/filesystem/clientLifecycle.test.ts b/tests/boundary/filesystem/clientLifecycle.test.ts
index 1333f465c7..0a649610b9 100644
--- a/tests/boundary/filesystem/clientLifecycle.test.ts
+++ b/tests/boundary/filesystem/clientLifecycle.test.ts
@@ -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(
diff --git a/tests/boundary/filesystem/clientRegistrationStatus.test.ts b/tests/boundary/filesystem/clientRegistrationStatus.test.ts
index 05279dae3c..d3cb002ead 100644
--- a/tests/boundary/filesystem/clientRegistrationStatus.test.ts
+++ b/tests/boundary/filesystem/clientRegistrationStatus.test.ts
@@ -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();
});
diff --git a/tests/boundary/filesystem/ompClientConfiguration.test.ts b/tests/boundary/filesystem/ompClientConfiguration.test.ts
new file mode 100644
index 0000000000..2815ec134c
--- /dev/null
+++ b/tests/boundary/filesystem/ompClientConfiguration.test.ts
@@ -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[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);
+ });
+});
diff --git a/tests/fixtures/clientEnvironment.ts b/tests/fixtures/clientEnvironment.ts
new file mode 100644
index 0000000000..f8657d25c9
--- /dev/null
+++ b/tests/fixtures/clientEnvironment.ts
@@ -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);
+};
diff --git a/tests/process-global/updateMaintenance.test.ts b/tests/process-global/updateMaintenance.test.ts
index c305d7136d..5686297191 100644
--- a/tests/process-global/updateMaintenance.test.ts
+++ b/tests/process-global/updateMaintenance.test.ts
@@ -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);
diff --git a/website/public/faq/index.html b/website/public/faq/index.html
index 40e5f616a1..60fe1183db 100644
--- a/website/public/faq/index.html
+++ b/website/public/faq/index.html
@@ -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.
Connect your coding agent.