* feat(clients): register Grok Build and Grok Bot during setup Detect both clients and install the local REA MCP registration the same way setup already handles the other agents. Grok Build edits only the REA tables in its TOML config and sets startup_timeout_sec to 30. Grok Bot writes the mcpServers.rea entry into mcp.json. That file is the registration to add to the signed-in account; setup does not call the account API. Filed by Grok. * fix(clients): clear a disabled Grok Build registration during setup Grok Build keeps a server suppressed when its name stays in disabled_mcp_servers, and setup was treating that file as already aligned. Setup now removes only that name, doctor reports the registration stale until it is gone, and uninstall keeps comments in an inline table whose only server was REA. Split the lifecycle suite and reflow the get-started page so static checks pass. * fix(verify): check packed Hopper search as literal-only The package verifier still called a regex probe that main deleted. The native bridge refuses regex and joins string hits with typed records, so the restored probe stubs that inventory and the check asserts the literal matches and the regex refusal. The new README translations also name Grok Build and Grok Bot. * fix(clients): stop treating a Grok Bot data directory as registered Grok Bot stores connectors in the signed-in account and runs them on its hosted computer. Setup no longer writes mcp.json or reports that file as aligned. Doctor and setup tell the caller to add the server from the Grok Bot chat. * fix(clients): drop multiline Grok disable-list entries Decode disabled_mcp_servers strings with the document parser so a multiline basic or literal string that loads as rea is removed. --------- Co-authored-by: Grok <eugene.ray@gmail.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
313 lines
9.6 KiB
TypeScript
313 lines
9.6 KiB
TypeScript
import { mkdir, writeFile } from "node:fs/promises";
|
|
import { join, resolve } from "node:path";
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.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);
|
|
});
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
describe("client registration status", () => {
|
|
it("discovers Claude Code from its config file without a marker directory", async () => {
|
|
const home = await createTestTempDirectory("rea-claude-config-only-");
|
|
await writeFile(
|
|
join(home, ".claude.json"),
|
|
JSON.stringify({
|
|
mcpServers: {
|
|
rea: {
|
|
command: "npx",
|
|
args: ["-y", PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp"],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
const statuses = await readClientRegistrationStatuses(home, "/current/rea");
|
|
|
|
expect(statuses).toEqual([
|
|
expect.objectContaining({
|
|
client: "claude_code",
|
|
config_path: join(home, ".claude.json"),
|
|
state: "aligned",
|
|
remediation: null,
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("distinguishes aligned, stale, missing, and invalid registrations", async () => {
|
|
const home = await createTestTempDirectory("rea-registrations-");
|
|
await Promise.all([
|
|
mkdir(join(home, ".codex")),
|
|
mkdir(join(home, ".cursor")),
|
|
mkdir(join(home, ".gemini")),
|
|
mkdir(join(home, ".claude")),
|
|
]);
|
|
await writeFile(
|
|
join(home, ".codex/config.toml"),
|
|
`[mcp_servers.rea]\ncommand = "npx"\nargs = ["-y", "${PRODUCT_IDENTITY.registrationPackageSpecifier}", "mcp"]\nstartup_timeout_sec = 30\n`,
|
|
);
|
|
await writeFile(
|
|
join(home, ".cursor/mcp.json"),
|
|
JSON.stringify({
|
|
mcpServers: {
|
|
rea: { command: "/old/rea", args: ["mcp"], env: { SECRET: "x" } },
|
|
},
|
|
}),
|
|
);
|
|
await writeFile(join(home, ".gemini/settings.json"), "not-json");
|
|
|
|
const statuses = await readClientRegistrationStatuses(home, "/current/rea");
|
|
expect(statuses).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
client: "codex",
|
|
state: "aligned",
|
|
remediation: null,
|
|
}),
|
|
expect.objectContaining({
|
|
client: "cursor",
|
|
state: "stale",
|
|
command: ["/old/rea", "mcp"],
|
|
remediation: expect.any(String),
|
|
}),
|
|
expect.objectContaining({
|
|
client: "gemini_cli",
|
|
state: "invalid",
|
|
command: [],
|
|
remediation: expect.any(String),
|
|
}),
|
|
expect.objectContaining({
|
|
client: "claude_code",
|
|
state: "missing",
|
|
command: [],
|
|
remediation: expect.any(String),
|
|
}),
|
|
]),
|
|
);
|
|
expect(JSON.stringify(statuses)).not.toContain("SECRET");
|
|
});
|
|
|
|
it.each([null, [], "invalid"])(
|
|
"reports a malformed server table %j as invalid",
|
|
async (servers) => {
|
|
const home = await createTestTempDirectory("rea-registration-shape-");
|
|
await mkdir(join(home, ".cursor"));
|
|
await writeFile(
|
|
join(home, ".cursor/mcp.json"),
|
|
JSON.stringify({ mcpServers: servers }),
|
|
);
|
|
expect(await readClientRegistrationStatuses(home)).toEqual([
|
|
expect.objectContaining({
|
|
client: "cursor",
|
|
state: "invalid",
|
|
command: [],
|
|
}),
|
|
]);
|
|
},
|
|
);
|
|
|
|
it("reports an unversioned npx registration as stale", async () => {
|
|
const home = await createTestTempDirectory("rea-registrations-");
|
|
await mkdir(join(home, ".codex"));
|
|
await writeFile(
|
|
join(home, ".codex/config.toml"),
|
|
'[mcp_servers.rea]\ncommand = "npx"\nargs = ["-y", "rea-agents", "mcp"]\n',
|
|
);
|
|
|
|
const statuses = await readClientRegistrationStatuses(home);
|
|
|
|
expect(statuses).toEqual([
|
|
expect.objectContaining({
|
|
client: "codex",
|
|
command: ["npx", "-y", "rea-agents", "mcp"],
|
|
state: "stale",
|
|
}),
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("commandcode registration status", () => {
|
|
const writeCommandcodeRegistration = async (
|
|
home: string,
|
|
registration: Record<string, unknown>,
|
|
): Promise<void> => {
|
|
await mkdir(join(home, ".commandcode"));
|
|
await writeFile(
|
|
join(home, ".commandcode/mcp.json"),
|
|
JSON.stringify({ mcpServers: { rea: registration } }),
|
|
);
|
|
};
|
|
|
|
const ownedArgs = (): readonly string[] => [
|
|
"-y",
|
|
PRODUCT_IDENTITY.registrationPackageSpecifier,
|
|
"mcp",
|
|
];
|
|
|
|
it.each([undefined, "sse"])(
|
|
"reports transport %j as invalid",
|
|
async (transport) => {
|
|
const home = await createTestTempDirectory("rea-commandcode-transport-");
|
|
await writeCommandcodeRegistration(home, {
|
|
...(transport === undefined ? {} : { transport }),
|
|
command: "npx",
|
|
args: [...ownedArgs()],
|
|
});
|
|
|
|
expect(await readClientRegistrationStatuses(home)).toEqual([
|
|
expect.objectContaining({
|
|
client: "commandcode",
|
|
state: "invalid",
|
|
command: [],
|
|
}),
|
|
]);
|
|
},
|
|
);
|
|
|
|
it("reports a disabled registration as stale", async () => {
|
|
const home = await createTestTempDirectory("rea-commandcode-disabled-");
|
|
await writeCommandcodeRegistration(home, {
|
|
transport: "stdio",
|
|
enabled: false,
|
|
command: "npx",
|
|
args: [...ownedArgs()],
|
|
});
|
|
|
|
expect(await readClientRegistrationStatuses(home)).toEqual([
|
|
expect.objectContaining({
|
|
client: "commandcode",
|
|
state: "stale",
|
|
}),
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("Node-wrapped registration policy", () => {
|
|
it.each([1, 30])(
|
|
"checks Codex startup timeout %d independently of launcher",
|
|
async (timeout) => {
|
|
const home = await createTestTempDirectory("rea-node-registration-");
|
|
await mkdir(join(home, ".codex"));
|
|
const entry = resolve("scripts/rea.mjs");
|
|
await writeFile(
|
|
join(home, ".codex/config.toml"),
|
|
`[mcp_servers.rea]\ncommand = ${JSON.stringify(process.execPath)}\nargs = [${JSON.stringify(entry)}, "mcp"]\nstartup_timeout_sec = ${String(timeout)}\n`,
|
|
);
|
|
const statuses = await readClientRegistrationStatuses(home, entry, {
|
|
environment: {},
|
|
});
|
|
expect(statuses).toEqual([
|
|
expect.objectContaining({
|
|
client: "codex",
|
|
state: timeout === 30 ? "aligned" : "stale",
|
|
}),
|
|
]);
|
|
},
|
|
);
|
|
|
|
it.each([1, 30])(
|
|
"checks Grok Build startup timeout %d independently of launcher",
|
|
async (timeout) => {
|
|
const home = await createTestTempDirectory("rea-grok-registration-");
|
|
await mkdir(join(home, ".grok"));
|
|
const entry = resolve("scripts/rea.mjs");
|
|
await writeFile(
|
|
join(home, ".grok/config.toml"),
|
|
`[mcp_servers.rea]\ncommand = ${JSON.stringify(process.execPath)}\nargs = [${JSON.stringify(entry)}, "mcp"]\nstartup_timeout_sec = ${String(timeout)}\n`,
|
|
);
|
|
const statuses = await readClientRegistrationStatuses(home, entry, {
|
|
environment: {},
|
|
});
|
|
expect(statuses).toEqual([
|
|
expect.objectContaining({
|
|
client: "grok_build",
|
|
state: timeout === 30 ? "aligned" : "stale",
|
|
}),
|
|
]);
|
|
},
|
|
);
|
|
|
|
it("reports Grok Build stale when rea is listed in disabled_mcp_servers", async () => {
|
|
const home = await createTestTempDirectory("rea-grok-disabled-status-");
|
|
await mkdir(join(home, ".grok"));
|
|
const entry = resolve("scripts/rea.mjs");
|
|
await writeFile(
|
|
join(home, ".grok/config.toml"),
|
|
`disabled_mcp_servers = ["rea"]\n[mcp_servers.rea]\ncommand = ${JSON.stringify(process.execPath)}\nargs = [${JSON.stringify(entry)}, "mcp"]\nstartup_timeout_sec = 30\n`,
|
|
);
|
|
expect(
|
|
await readClientRegistrationStatuses(home, entry, { environment: {} }),
|
|
).toEqual([
|
|
expect.objectContaining({ client: "grok_build", state: "stale" }),
|
|
]);
|
|
});
|
|
|
|
it("checks a direct-launcher Codex startup timeout without Node wrapping", async () => {
|
|
const home = await createTestTempDirectory("rea-direct-registration-");
|
|
await mkdir(join(home, ".codex"));
|
|
await writeFile(
|
|
join(home, ".codex/config.toml"),
|
|
'[mcp_servers.rea]\ncommand = "/current/rea"\nargs = ["mcp"]\nstartup_timeout_sec = 1\n',
|
|
);
|
|
const statuses = await readClientRegistrationStatuses(
|
|
home,
|
|
"/current/rea",
|
|
{
|
|
environment: {},
|
|
},
|
|
);
|
|
expect(statuses).toEqual([
|
|
expect.objectContaining({ client: "codex", state: "stale" }),
|
|
]);
|
|
});
|
|
|
|
it.each([{ tools: ["binary_session"] }, { tools: ["*"] }])(
|
|
"checks Copilot tool selection $tools independently of launcher",
|
|
async ({ tools }) => {
|
|
const home = await createTestTempDirectory("rea-node-copilot-");
|
|
await mkdir(join(home, ".copilot"));
|
|
const entry = resolve("scripts/rea.mjs");
|
|
await writeFile(
|
|
join(home, ".copilot/mcp-config.json"),
|
|
JSON.stringify({
|
|
mcpServers: {
|
|
rea: {
|
|
type: "stdio",
|
|
command: process.execPath,
|
|
args: [entry, "mcp"],
|
|
tools,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
const statuses = await readClientRegistrationStatuses(home, entry, {
|
|
environment: {},
|
|
});
|
|
expect(statuses).toEqual([
|
|
expect.objectContaining({
|
|
client: "copilot_cli",
|
|
state: tools.includes("*") ? "aligned" : "stale",
|
|
}),
|
|
]);
|
|
},
|
|
);
|
|
});
|