mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
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:
co-authored by
Claude Opus 5.5
parent
24673aaae4
commit
8b4c359135
@@ -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
@@ -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
|
||||
|
||||
@@ -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 ?? ""}`,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Vendored
+22
@@ -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);
|
||||
|
||||
@@ -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>.
|
||||
|
||||
Reference in New Issue
Block a user