Compare commits

...
Author SHA1 Message Date
Cursor Agent 2f2110a8ba 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, sets startup_timeout_sec to 30, and
removes rea from disabled_mcp_servers. Grok Bot writes mcpServers.rea
into mcp.json. Setup does not call the account API.

Uninstall keeps comments in an inline table whose only server was REA.
2026-10-08 21:33:27 +00:00
23 changed files with 2243 additions and 35 deletions
+1 -1
View File
@@ -70,7 +70,7 @@ Choose your agents, review the proposed changes, and approve them. Setup adds
REA's MCP server and matching workflow instructions, with backups of existing
configuration. Restart your agent afterward.
Setup supports Claude Code, Codex, Cursor, Gemini CLI and
Setup supports Claude Code, Codex, Cursor, Gemini CLI, Grok Build, Grok Bot and
[other agents](docs/installation.md#supported-agents). See
[installation and setup](docs/installation.md) for provider configuration and
manual MCP registration.
+1 -1
View File
@@ -68,7 +68,7 @@ npx rea-agents setup
اختر الوكلاء، وراجع التغييرات المقترحة، ثم وافق عليها. يضيف الإعداد خادم MCP الخاص بـ REA وتعليمات سير العمل المطابقة، مع الاحتفاظ بنسخ احتياطية من الإعدادات الموجودة. أعد تشغيل وكيلك بعد ذلك.
يدعم الإعداد Claude Code وCodex وCursor وGemini CLI و[وكلاء آخرين](docs/installation.md#supported-agents). راجع [التثبيت والإعداد](docs/installation.md) لتهيئة موفّري التحليل وتسجيل MCP يدويًا.
يدعم الإعداد Claude Code وCodex وCursor وGemini CLI وGrok Build وGrok Bot و[وكلاء آخرين](docs/installation.md#supported-agents). راجع [التثبيت والإعداد](docs/installation.md) لتهيئة موفّري التحليل وتسجيل MCP يدويًا.
### اطلب من وكيلك
+1 -1
View File
@@ -68,7 +68,7 @@ npx rea-agents setup
使うエージェントを選び、予定されている変更を確認して承認してください。セットアップは REA の MCP サーバーと対応するワークフローの手順を追加し、既存の設定をバックアップします。完了後にエージェントを再起動してください。
セットアップは Claude Code、Codex、Cursor、Gemini CLI、および[その他のエージェント](docs/installation.md#supported-agents)に対応しています。プロバイダーの設定と MCP の手動登録については、[インストールとセットアップ](docs/installation.md)を参照してください。
セットアップは Claude Code、Codex、Cursor、Gemini CLI、Grok Build、Grok Bot、および[その他のエージェント](docs/installation.md#supported-agents)に対応しています。プロバイダーの設定と MCP の手動登録については、[インストールとセットアップ](docs/installation.md)を参照してください。
### エージェントに依頼する
+1 -1
View File
@@ -68,7 +68,7 @@ npx rea-agents setup
에이전트를 선택하고, 예정된 변경 사항을 검토한 뒤 승인하세요. 설정 과정은 기존 설정을 백업하고 REA의 MCP 서버와 버전에 맞는 워크플로 지침을 추가합니다. 완료 후 에이전트를 다시 시작하세요.
설정은 Claude Code, Codex, Cursor, Gemini CLI 및 [다른 에이전트](docs/installation.md#supported-agents)를 지원합니다. 제공자 설정과 수동 MCP 등록은 [설치 및 설정](docs/installation.md)을 참고하세요.
설정은 Claude Code, Codex, Cursor, Gemini CLI, Grok Build, Grok Bot 및 [다른 에이전트](docs/installation.md#supported-agents)를 지원합니다. 제공자 설정과 수동 MCP 등록은 [설치 및 설정](docs/installation.md)을 참고하세요.
### 에이전트에게 요청
+1 -1
View File
@@ -68,7 +68,7 @@ npx rea-agents setup
选择智能体,检查计划中的变更并批准。设置流程会添加 REA 的 MCP 服务和匹配的工作流指引,并备份已有配置。完成后重启智能体。
设置流程支持 Claude Code、Codex、Cursor、Gemini CLI 和[其他智能体](docs/installation.md#supported-agents)。提供方配置和手动注册 MCP 的方法见[安装与设置](docs/installation.md)。
设置流程支持 Claude Code、Codex、Cursor、Gemini CLI、Grok Build、Grok Bot 和[其他智能体](docs/installation.md#supported-agents)。提供方配置和手动注册 MCP 的方法见[安装与设置](docs/installation.md)。
### 询问智能体
+16 -1
View File
@@ -135,12 +135,27 @@ Setup can configure these clients for REA's local MCP server:
| GitHub Copilot CLI | `copilot_cli` |
| Command Code | `commandcode` |
| VS Code | `vscode` |
| Grok Build | `grok_build` |
| Grok Bot | `grok_bot` |
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
`mcp.servers` table, setup registers REA there instead and replaces any earlier
`mcp.rea` entry from REA.
Grok Build loads `[mcp_servers.rea]` from `$GROK_HOME/config.toml`, or from
`~/.grok/config.toml` when `GROK_HOME` is unset. Setup edits that server
table, `[mcp_servers.rea.env]`, and a root `disabled_mcp_servers` entry that
names `rea`. It sets `startup_timeout_sec = 30` and leaves every other name
in that list. The shared skill installed under `~/.agents/skills` is already
on Grok Build's skill path.
Grok Bot is detected from `~/.grokbot`, or from `SAND_DATA_ROOT` when that
value is an absolute path. Setup writes `mcpServers.rea` to `mcp.json` in that
directory. The desktop app keeps the connectors it actually runs in the
signed-in account. That file is the registration to add there; setup does not
call the account API.
## Review setup changes
`rea setup` first offers the supported agents in a multi-select. Existing REA
@@ -209,7 +224,7 @@ rea setup --yes --all-detected --install-hopper --json
Setup pins package-runner MCP registrations to the exact installed REA version,
installs the matching skill and on-demand references in the same plan, and adds
`startup_timeout_sec = 30` for Codex. `rea update` installs the exact resolved
`startup_timeout_sec = 30` for Codex and Grok Build. `rea update` installs the exact resolved
release into the npm prefix that owns the running package, then checks the new
executable's version before reporting success. It does not reopen onboarding.
Release lookup and installation both use npm's configured registry.
+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, and VS Code using each client's configuration format.
GitHub Copilot CLI, Command Code, VS Code, Grok Build, and Grok Bot using each client's configuration format.
Ghidra analysis supports Linux x64 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
+2
View File
@@ -79,6 +79,8 @@ export async function verifyPackageEnvironment({
CLAUDE_CONFIG_DIR: home,
CODEX_HOME: codexDir,
COPILOT_HOME: join(home, ".copilot"),
GROK_HOME: join(home, ".grok"),
SAND_DATA_ROOT: join(home, ".grokbot"),
OPENCODE_CONFIG: undefined,
XDG_CONFIG_HOME: join(home, ".config"),
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
+2
View File
@@ -101,6 +101,8 @@ export async function verifyPackageUpdate({
CLAUDE_CONFIG_DIR: home,
CODEX_HOME: join(home, ".codex"),
COPILOT_HOME: join(home, ".copilot"),
GROK_HOME: join(home, ".grok"),
SAND_DATA_ROOT: join(home, ".grokbot"),
XDG_CONFIG_HOME: join(home, ".config"),
OPENCODE_CONFIG: join(home, ".config", "opencode", "opencode.json"),
npm_config_prefix: prefix,
+9
View File
@@ -99,6 +99,15 @@ async function verifyPublishedUpdate({ version: targetVersion, canaryRoot }) {
const environment = {
...process.env,
HOME: home,
USERPROFILE: home,
APPDATA: join(home, "AppData", "Roaming"),
CLAUDE_CONFIG_DIR: home,
CODEX_HOME: join(home, ".codex"),
COPILOT_HOME: join(home, ".copilot"),
GROK_HOME: join(home, ".grok"),
SAND_DATA_ROOT: join(home, ".grokbot"),
XDG_CONFIG_HOME: join(home, ".config"),
OPENCODE_CONFIG: join(home, ".config", "opencode", "opencode.json"),
npm_config_cache: join(canaryRoot, "update-cache"),
npm_config_prefix: prefix,
PATH: `${join(prefix, "bin")}${delimiter}${process.env.PATH ?? ""}`,
+4
View File
@@ -108,9 +108,13 @@ try {
HOME: home,
USERPROFILE: home,
APPDATA: join(home, "AppData", "Roaming"),
CLAUDE_CONFIG_DIR: home,
CODEX_HOME: join(home, ".codex"),
XDG_CONFIG_HOME: join(home, ".config"),
OPENCODE_CONFIG: join(home, ".config", "opencode", "opencode.jsonc"),
COPILOT_HOME: join(home, ".copilot"),
GROK_HOME: join(home, ".grok"),
SAND_DATA_ROOT: join(home, ".grokbot"),
};
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`, or `vscode`. If the client is unknown, use `doctor --json` and
`commandcode`, `vscode`, `grok_build`, or `grok_bot`. If the client is unknown, use `doctor --json` and
inspect its registration results before choosing a setup scope.
2. Distinguish the reason. Missing, malformed, or stale registration needs a
scoped configuration repair. An aligned registration with no tools in the
File diff suppressed because it is too large Load Diff
+13 -5
View File
@@ -1,5 +1,6 @@
import {
effectiveClientServer,
grokServerListedDisabled,
parseClientConfiguration,
} from "./ClientConfigurationDocument.js";
import { access, readFile } from "node:fs/promises";
@@ -80,7 +81,9 @@ export const readClientRegistrationStatuses = async (
CLAUDE_CONFIG_DIR: options.environment.CLAUDE_CONFIG_DIR,
CODEX_HOME: options.environment.CODEX_HOME,
COPILOT_HOME: options.environment.COPILOT_HOME,
GROK_HOME: options.environment.GROK_HOME,
OPENCODE_CONFIG: options.environment.OPENCODE_CONFIG,
SAND_DATA_ROOT: options.environment.SAND_DATA_ROOT,
XDG_CONFIG_HOME: options.environment.XDG_CONFIG_HOME,
},
)) {
@@ -91,10 +94,8 @@ export const readClientRegistrationStatuses = async (
continue;
try {
const content = await readFile(client.configPath, "utf8");
const raw = effectiveClientServer(
parseClientConfiguration(content, client.format),
PRODUCT_IDENTITY.mcpServerKey,
);
const parsed = parseClientConfiguration(content, client.format);
const raw = effectiveClientServer(parsed, PRODUCT_IDENTITY.mcpServerKey);
if (raw === undefined) {
statuses.push(
unavailableStatus(client.name, client.configPath, "missing"),
@@ -116,6 +117,13 @@ export const readClientRegistrationStatuses = async (
client,
currentCommandPath,
options.platform ?? process.platform,
) &&
!(
client.format === "grok" &&
grokServerListedDisabled(
parsed.document,
PRODUCT_IDENTITY.mcpServerKey,
)
)
? "aligned"
: "stale",
@@ -148,7 +156,7 @@ const registrationAligned = (
if (!isOwnedClientRegistrationCommand(command, currentCommandPath))
return false;
if (
client.name === "codex" &&
(client.name === "codex" || client.name === "grok_build") &&
registration.startup_timeout_sec !==
MCP_STARTUP_POLICY.codexStartupTimeoutSeconds
)
+10 -4
View File
@@ -2,6 +2,7 @@ import {
clientRegistrationEntry,
clientConfigurationValuesEqual,
clientServerPath,
grokServerListedDisabled,
legacyClientServerPath,
parseClientConfiguration,
serializeClientConfiguration,
@@ -58,7 +59,7 @@ const configureClientDocument = async (
let parsed: ClientConfigurationDocument;
try {
parsed = parseClientConfiguration(
original ?? (format === "toml" ? "" : "{}"),
original ?? (format === "toml" || format === "grok" ? "" : "{}"),
format,
);
} catch (cause: unknown) {
@@ -234,7 +235,7 @@ const restoreConfig = async (
}
};
/** Whether REA's entry matches and no conflicting legacy entry remains. */
/** Whether REA's entry matches, no legacy entry remains, and Grok is not suppressing it. */
const registrationCurrent = (
parsed: ClientConfigurationDocument,
desired: unknown,
@@ -242,7 +243,12 @@ const registrationCurrent = (
clientConfigurationValuesEqual(
parsed.servers[PRODUCT_IDENTITY.mcpServerKey],
desired,
) && !Object.hasOwn(parsed.legacyServers, PRODUCT_IDENTITY.mcpServerKey);
) &&
!Object.hasOwn(parsed.legacyServers, PRODUCT_IDENTITY.mcpServerKey) &&
!(
parsed.dialect === "grok" &&
grokServerListedDisabled(parsed.document, PRODUCT_IDENTITY.mcpServerKey)
);
const clientConfigurationDesired = (
client: SetupClient,
@@ -262,7 +268,7 @@ const clientConfigurationDesired = (
);
return {
...registration,
...(client.name === "codex"
...(client.name === "codex" || client.name === "grok_build"
? {
startup_timeout_sec: MCP_STARTUP_POLICY.codexStartupTimeoutSeconds,
}
+33 -1
View File
@@ -1,4 +1,4 @@
import { join } from "node:path";
import { isAbsolute, join } from "node:path";
import { lstatSync } from "node:fs";
/** One supported client configuration location. */
@@ -14,6 +14,7 @@ export interface SetupClient {
| "copilot_cli"
| "opencode"
| "commandcode"
| "grok"
| "unsupported";
}
@@ -27,7 +28,9 @@ interface ClientPathContext {
readonly CLAUDE_CONFIG_DIR?: string | undefined;
readonly CODEX_HOME?: string | undefined;
readonly COPILOT_HOME?: string | undefined;
readonly GROK_HOME?: string | undefined;
readonly OPENCODE_CONFIG?: string | undefined;
readonly SAND_DATA_ROOT?: string | undefined;
readonly XDG_CONFIG_HOME?: string | undefined;
};
}
@@ -77,6 +80,17 @@ const claudeCodeMarkerDirectory = ({ home, env }: ClientPathContext): string =>
const codexDirectory = ({ home, env }: ClientPathContext): string =>
env.CODEX_HOME ?? join(home, ".codex");
const grokDirectory = ({ home, env }: ClientPathContext): string =>
env.GROK_HOME ?? join(home, ".grok");
/** Grok Bot uses an absolute SAND_DATA_ROOT; anything else stays ~/.grokbot. */
const grokBotDirectory = ({ home, env }: ClientPathContext): string => {
const root = env.SAND_DATA_ROOT;
return root !== undefined && root !== "" && isAbsolute(root)
? root
: join(home, ".grokbot");
};
const copilotDirectory = ({ home, env }: ClientPathContext): string =>
env.COPILOT_HOME ?? join(home, ".copilot");
@@ -212,6 +226,22 @@ export const SUPPORTED_CLIENT_DEFINITIONS = [
markerPath: vscodeUserDirectory,
format: "vscode",
},
{
name: "grok_build",
displayName: "Grok Build",
configPath: (context: ClientPathContext) =>
join(grokDirectory(context), "config.toml"),
markerPath: grokDirectory,
format: "grok",
},
{
name: "grok_bot",
displayName: "Grok Bot",
configPath: (context: ClientPathContext) =>
join(grokBotDirectory(context), "mcp.json"),
markerPath: grokBotDirectory,
format: "json",
},
] as const satisfies readonly ClientDefinition[];
const resolvePath = (path: ClientPath, context: ClientPathContext): string =>
@@ -226,7 +256,9 @@ export const supportedClients = (
CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR,
CODEX_HOME: process.env.CODEX_HOME,
COPILOT_HOME: process.env.COPILOT_HOME,
GROK_HOME: process.env.GROK_HOME,
OPENCODE_CONFIG: process.env.OPENCODE_CONFIG,
SAND_DATA_ROOT: process.env.SAND_DATA_ROOT,
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
},
): readonly SetupClient[] => {
+1 -1
View File
@@ -152,7 +152,7 @@ const removeClient = async (
return item(
client.name,
"failed",
`Configuration is not valid ${client.format === "toml" ? "TOML" : client.format === "opencode" ? "JSONC" : "JSON"} and was not changed. Repair it, then rerun uninstall.`,
`Configuration is not valid ${client.format === "toml" || client.format === "grok" ? "TOML" : client.format === "opencode" ? "JSONC" : "JSON"} and was not changed. Repair it, then rerun uninstall.`,
);
}
const name = PRODUCT_IDENTITY.mcpServerKey;
@@ -25,7 +25,9 @@ beforeEach(() => {
"CLAUDE_CONFIG_DIR",
"CODEX_HOME",
"COPILOT_HOME",
"GROK_HOME",
"OPENCODE_CONFIG",
"SAND_DATA_ROOT",
"XDG_CONFIG_HOME",
])
vi.stubEnv(name, undefined);
@@ -398,6 +400,40 @@ describe("platform-aware client config paths", () => {
},
);
it("honors Grok Build and Grok Bot configuration roots", () => {
expect(
supportedClients("/home/a", "linux", {
GROK_HOME: "/custom/grok",
SAND_DATA_ROOT: "/custom/grokbot",
}).filter(({ name }) => name === "grok_build" || name === "grok_bot"),
).toEqual([
expect.objectContaining({
name: "grok_build",
displayName: "Grok Build",
configPath: "/custom/grok/config.toml",
markerPath: "/custom/grok",
format: "grok",
}),
expect.objectContaining({
name: "grok_bot",
displayName: "Grok Bot",
configPath: "/custom/grokbot/mcp.json",
markerPath: "/custom/grokbot",
format: "json",
}),
]);
expect(
supportedClients("/home/a", "linux", {
SAND_DATA_ROOT: "relative/sand",
}).find(({ name }) => name === "grok_bot")?.configPath,
).toBe("/home/a/.grokbot/mcp.json");
expect(
supportedClients("/home/a", "darwin", {}).find(
({ name }) => name === "grok_build",
)?.configPath,
).toBe("/home/a/.grok/config.toml");
});
it("honors Copilot CLI and Devin platform configuration roots", () => {
expect(
supportedClients("/home/a", "linux", {
File diff suppressed because it is too large Load Diff
@@ -1,13 +1,30 @@
import { mkdir, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { describe, expect, it } from "vitest";
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-");
@@ -206,6 +223,43 @@ describe("Node-wrapped registration policy", () => {
},
);
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"));
@@ -27,6 +27,8 @@ beforeEach(async () => {
CLAUDE_CONFIG_DIR: home,
CODEX_HOME: join(home, ".codex"),
COPILOT_HOME: join(home, ".copilot"),
GROK_HOME: join(home, ".grok"),
SAND_DATA_ROOT: join(home, ".grokbot"),
XDG_CONFIG_HOME: join(home, ".config"),
OPENCODE_CONFIG: join(home, ".config", "opencode", "opencode.jsonc"),
}))
+1 -1
View File
@@ -107,7 +107,7 @@
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 and VS Code.
Command Code, Grok Build, Grok Bot and VS Code.
</p>
<p>
<a href="../get-started/#agent">Connect your coding agent</a>.
+2 -2
View File
@@ -75,8 +75,8 @@
<h2>Connect your coding agent</h2>
<p>
Run setup to connect REA to your coding agent. Client choices
include Claude Code, Codex, Cursor, Gemini CLI, Windsurf and other
supported MCP clients.
include Claude Code, Codex, Cursor, Gemini CLI, Windsurf, Grok
Build, Grok Bot and other supported MCP clients.
</p>
<div class="agent-terminal">
<div class="terminal-header">