feat(identity): rename package and CLI to REA

This commit is contained in:
morluto 2026-07-12 07:44:30 +08:00
parent 97541d8dad
commit 3d70812c37
17 changed files with 129 additions and 85 deletions

View File

@ -1,6 +1,6 @@
# Better Binary # REA
Agent-ready Hopper Disassembler tooling: a beta.3 MCP server with 31 official proxies, 8 enhanced reverse-engineering workflows, and 3 binary-session tools. REA is agent-ready Hopper Disassembler tooling: one CLI with a beta.3 MCP mode, 31 official proxies, 8 enhanced reverse-engineering workflows, and 3 binary-session tools.
## Install from this checkout ## Install from this checkout
@ -10,24 +10,26 @@ Requires macOS 12+ and Node.js 22+.
npm ci npm ci
npm run build npm run build
npm link npm link
better-binary setup --yes rea setup --yes
``` ```
`setup` is safe to repeat. It checks the host, installs Homebrew when approved and absent, installs Hopper from the official `hopper-disassembler` cask, and finishes with structured diagnostics. It never bundles Hopper or publishes this private package. `setup` is safe to repeat. It checks the host, installs Homebrew when approved and absent, installs Hopper from the official `hopper-disassembler` cask, and finishes with structured diagnostics. It never bundles Hopper or publishes this private package.
The CLI and MCP adapter share the same binary-session core. Source-checkout development uses `npm link`; packaged verification uses a local tarball and does not require a published package.
Useful onboarding commands: Useful onboarding commands:
```bash ```bash
better-binary --help rea --help
better-binary --llms rea --llms
better-binary doctor --target /path/to/binary rea doctor --target /path/to/binary
better-binary skills add rea skills add
better-binary mcp add rea mcp add
``` ```
## MCP workflow ## MCP workflow
Start `better-binary-mcp` with no target. In the connected agent: Start the MCP adapter with `rea mcp` (or `rea --mcp`) with no target. In the connected agent:
1. Call `open_binary` with any readable local binary or `.hop` path. 1. Call `open_binary` with any readable local binary or `.hop` path.
2. Call `binary_overview`, then use decompilation, strings, symbols, and xrefs tools. 2. Call `binary_overview`, then use decompilation, strings, symbols, and xrefs tools.
@ -43,8 +45,9 @@ Manual target-free MCP configuration:
```json ```json
{ {
"mcpServers": { "mcpServers": {
"better-binary": { "rea": {
"command": "better-binary-mcp" "command": "npx",
"args": ["-y", "@morluto/rea", "mcp"]
} }
} }
} }
@ -57,6 +60,7 @@ The first `open_binary` can take time while Hopper analyzes the file. Analysis c
```bash ```bash
npm run check npm run check
npm pack --dry-run npm pack --dry-run
npm run verify:package
HOPPER_TARGET_PATH=/path/to/target-a \ HOPPER_TARGET_PATH=/path/to/target-a \
HOPPER_SECOND_TARGET_PATH=/path/to/distinct-target-b \ HOPPER_SECOND_TARGET_PATH=/path/to/distinct-target-b \
npm run verify:hopper npm run verify:hopper
@ -64,7 +68,14 @@ npm run verify:hopper
Real-Hopper verification requires Hopper and a representative target. The stdio server uses only the repository's authenticated Unix-socket bridge; Hopper's bundled MCP server is not used. Real-Hopper verification requires Hopper and a representative target. The stdio server uses only the repository's authenticated Unix-socket bridge; Hopper's bundled MCP server is not used.
The provisional package, executable, MCP, skill, and config names are centralized in `src/identity.ts` so release naming can change cleanly. The package remains private and is not ready for publication. The package, executable, MCP, skill, and config names are centralized in `src/identity.ts`. The package remains private and is not ready for publication.
The package deliberately ships only the `rea` executable. A future zero-install setup will use the floating registration `npx -y @morluto/rea mcp`; local tarballs exercise the same packaged command before publication:
```bash
npm pack
npm exec --yes --package ./morluto-rea-0.1.0.tgz -- rea --help
```
## License ## License

View File

@ -119,7 +119,7 @@ def _assembly(procedure):
def _dispatch(method, params): def _dispatch(method, params):
global _selected_document global _selected_document
if method == "health": if method == "health":
return {"name": "betterBinaryMCP Hopper bridge", "version": "1.0.0"} return {"name": "REA Hopper bridge", "version": "1.0.0"}
if method == "shutdown": if method == "shutdown":
return {"shutdown": True} return {"shutdown": True}
if method == "list_documents": if method == "list_documents":
@ -239,7 +239,7 @@ def _serve_connection(connection):
if set(request) != {"id", "token", "method", "params"}: if set(request) != {"id", "token", "method", "params"}:
raise ValueError("Invalid bridge request shape") raise ValueError("Invalid bridge request shape")
request_id = request["id"] request_id = request["id"]
if not isinstance(request["token"], str) or not hmac.compare_digest(request["token"], BETTER_BINARY_TOKEN): if not isinstance(request["token"], str) or not hmac.compare_digest(request["token"], REA_TOKEN):
raise PermissionError("Invalid bridge capability") raise PermissionError("Invalid bridge capability")
result = _dispatch(request["method"], request["params"]) result = _dispatch(request["method"], request["params"])
should_stop = request["method"] == "shutdown" should_stop = request["method"] == "shutdown"
@ -255,19 +255,19 @@ def _serve_connection(connection):
def _run(): def _run():
if os.path.exists(BETTER_BINARY_SOCKET): if os.path.exists(REA_SOCKET):
os.unlink(BETTER_BINARY_SOCKET) os.unlink(REA_SOCKET)
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(BETTER_BINARY_SOCKET) server.bind(REA_SOCKET)
os.chmod(BETTER_BINARY_SOCKET, 0o600) os.chmod(REA_SOCKET, 0o600)
server.listen(1) server.listen(1)
try: try:
connection, _ = server.accept() connection, _ = server.accept()
_serve_connection(connection) _serve_connection(connection)
finally: finally:
server.close() server.close()
if os.path.exists(BETTER_BINARY_SOCKET): if os.path.exists(REA_SOCKET):
os.unlink(BETTER_BINARY_SOCKET) os.unlink(REA_SOCKET)
# Hopper's public objects are bound to its dedicated Python execution thread. # Hopper's public objects are bound to its dedicated Python execution thread.

6
package-lock.json generated
View File

@ -1,11 +1,11 @@
{ {
"name": "better-binary-mcp", "name": "@morluto/rea",
"version": "0.1.0", "version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "better-binary-mcp", "name": "@morluto/rea",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@modelcontextprotocol/server": "2.0.0-beta.3", "@modelcontextprotocol/server": "2.0.0-beta.3",
@ -13,7 +13,7 @@
"zod": "4.4.3" "zod": "4.4.3"
}, },
"bin": { "bin": {
"better-binary-mcp": "dist/main.js" "rea": "scripts/rea.mjs"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "9.39.2", "@eslint/js": "9.39.2",

View File

@ -1,11 +1,10 @@
{ {
"name": "better-binary-mcp", "name": "@morluto/rea",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module", "type": "module",
"bin": { "bin": {
"better-binary": "./scripts/better-binary.mjs", "rea": "./scripts/rea.mjs"
"better-binary-mcp": "./dist/main.js"
}, },
"engines": { "engines": {
"node": ">=22" "node": ">=22"
@ -13,7 +12,7 @@
"files": [ "files": [
"dist", "dist",
"bridge", "bridge",
"scripts/better-binary.mjs", "scripts/rea.mjs",
"skills", "skills",
"README.md", "README.md",
"LICENSE" "LICENSE"
@ -31,7 +30,8 @@
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"verify:hopper": "npm run build && node scripts/verify-real-hopper.mjs", "verify:hopper": "npm run build && node scripts/verify-real-hopper.mjs",
"verify:package": "npm run build && node scripts/verify-package.mjs", "verify:package": "npm run build && node scripts/verify-package.mjs",
"config:print": "node scripts/print-mcp-config.mjs" "config:print": "node scripts/print-mcp-config.mjs",
"prepack": "npm run build"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/server": "2.0.0-beta.3", "@modelcontextprotocol/server": "2.0.0-beta.3",

View File

@ -9,6 +9,7 @@
*/ */
import { resolve } from "node:path"; import { resolve } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { readFile } from "node:fs/promises";
const targetPath = process.argv[2]; const targetPath = process.argv[2];
if (!targetPath) { if (!targetPath) {
@ -22,11 +23,6 @@ const flags = process.argv.slice(3);
const kind = readFlag(flags, "--kind") ?? "executable"; const kind = readFlag(flags, "--kind") ?? "executable";
const loaderArgs = readFlag(flags, "--loader-args"); const loaderArgs = readFlag(flags, "--loader-args");
const serverPath = resolve(
fileURLToPath(new URL("..", import.meta.url)),
"dist/main.js",
);
const env = { const env = {
HOPPER_TARGET_PATH: resolve(targetPath), HOPPER_TARGET_PATH: resolve(targetPath),
HOPPER_TARGET_KIND: kind, HOPPER_TARGET_KIND: kind,
@ -35,9 +31,21 @@ if (loaderArgs) env.HOPPER_LOADER_ARGS_JSON = loaderArgs;
const config = { const config = {
mcpServers: { mcpServers: {
betterBinaryMCP: { rea: {
command: "node", command: "npx",
args: [serverPath], args: [
"-y",
JSON.parse(
await readFile(
resolve(
fileURLToPath(new URL("..", import.meta.url)),
"package.json",
),
"utf8",
),
).name,
"mcp",
],
env, env,
}, },
}, },

View File

@ -16,7 +16,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
const exec = promisify(execFile); const exec = promisify(execFile);
const root = process.cwd(); const root = process.cwd();
const workspace = await mkdtemp(join(tmpdir(), "better-binary-package-")); const workspace = await mkdtemp(join(tmpdir(), "rea-package-"));
let tarball; let tarball;
try { try {
@ -39,7 +39,7 @@ try {
await writeFile(swVers, "#!/bin/sh\necho 14.5\n"); await writeFile(swVers, "#!/bin/sh\necho 14.5\n");
await writeFile( await writeFile(
npx, npx,
'#!/bin/sh\nprintf "%s\\n" "$*" > "$BETTER_BINARY_NPX_LOG"\necho "│ ✓ Cursor: isolated │"\n', '#!/bin/sh\nprintf "%s\\n" "$*" > "$REA_NPX_LOG"\necho "│ ✓ Cursor: isolated │"\n',
); );
await writeFile(hopper, "#!/bin/sh\nexit 0\n"); await writeFile(hopper, "#!/bin/sh\nexit 0\n");
await Promise.all([ await Promise.all([
@ -64,15 +64,14 @@ try {
XDG_CONFIG_HOME: join(home, ".config"), XDG_CONFIG_HOME: join(home, ".config"),
PATH: `${fakeBin}:${process.env.PATH ?? ""}`, PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
HOPPER_LAUNCHER_PATH: hopper, HOPPER_LAUNCHER_PATH: hopper,
BETTER_BINARY_NPX_LOG: npxLog, REA_NPX_LOG: npxLog,
}; };
await exec( await exec(
"npm", "npm",
["install", "--global", "--prefix", prefix, join(root, tarball)], ["install", "--global", "--prefix", prefix, join(root, tarball)],
{ env: environment }, { env: environment },
); );
const cli = join(prefix, "bin", "better-binary"); const cli = join(prefix, "bin", "rea");
const mcp = join(prefix, "bin", "better-binary-mcp");
const help = await run(cli, ["--help"], environment); const help = await run(cli, ["--help"], environment);
const llms = await run(cli, ["--llms"], environment); const llms = await run(cli, ["--llms"], environment);
const doctor = json(await run(cli, ["doctor", "--json"], environment)); const doctor = json(await run(cli, ["doctor", "--json"], environment));
@ -84,10 +83,8 @@ try {
throw new Error("packaged CLI discovery or doctor failed"); throw new Error("packaged CLI discovery or doctor failed");
await run(cli, ["mcp", "add"], environment); await run(cli, ["mcp", "add"], environment);
const mcpRegistration = await readFile(npxLog, "utf8"); const mcpRegistration = await readFile(npxLog, "utf8");
if ( if (!mcpRegistration.includes("add-mcp npx -y @morluto/rea mcp --name rea"))
!mcpRegistration.includes("add-mcp better-binary-mcp --name better-binary") throw new Error("Incur mcp add did not register the floating npx command");
)
throw new Error("Incur mcp add did not register the beta.3 executable");
const first = json(await run(cli, ["setup", "--yes", "--json"], environment)); const first = json(await run(cli, ["setup", "--yes", "--json"], environment));
const second = json( const second = json(
await run(cli, ["setup", "--yes", "--json"], environment), await run(cli, ["setup", "--yes", "--json"], environment),
@ -102,18 +99,18 @@ try {
const config = json(await readFile(configPath, "utf8")); const config = json(await readFile(configPath, "utf8"));
if ( if (
config.existing !== true || config.existing !== true ||
config.mcpServers?.["better-binary"]?.command !== "better-binary-mcp" config.mcpServers?.rea?.command !== "npx" ||
JSON.stringify(config.mcpServers?.rea?.args) !==
JSON.stringify(["-y", "@morluto/rea", "mcp"])
) )
throw new Error("packaged client readback failed"); throw new Error("packaged client readback failed");
if ( if (
!(await readFile(`${configPath}.better-binary.backup`, "utf8")).includes( !(await readFile(`${configPath}.rea.backup`, "utf8")).includes("existing")
"existing",
)
) )
throw new Error("packaged client backup failed"); throw new Error("packaged client backup failed");
} }
const skill = await readFile( const skill = await readFile(
join(home, ".agents/skills/better-binary-analysis/SKILL.md"), join(home, ".agents/skills/rea-analysis/SKILL.md"),
"utf8", "utf8",
); );
if (!skill.includes("open_binary")) if (!skill.includes("open_binary"))
@ -136,7 +133,8 @@ try {
throw new Error("packaged setup did not recover"); throw new Error("packaged setup did not recover");
const transport = new StdioClientTransport({ const transport = new StdioClientTransport({
command: mcp, command: cli,
args: ["mcp"],
env: environment, env: environment,
stderr: "pipe", stderr: "pipe",
}); });
@ -163,7 +161,7 @@ try {
} }
process.stdout.write( process.stdout.write(
`${JSON.stringify({ cli: true, incurMcpCommand: "better-binary-mcp", doctor: true, setup: "idempotent", clients: 2, backupReadback: true, failureRecovery: true, skill: true, mcpTools: 42, targetFree: true })}\n`, `${JSON.stringify({ cli: true, incurMcpCommand: "npx -y @morluto/rea mcp", doctor: true, setup: "idempotent", clients: 2, backupReadback: true, failureRecovery: true, skill: true, mcpTools: 42, targetFree: true })}\n`,
); );
} finally { } finally {
if (tarball) await rm(join(root, tarball), { force: true }); if (tarball) await rm(join(root, tarball), { force: true });

View File

@ -1,8 +1,8 @@
--- ---
name: better-binary-analysis name: rea-analysis
description: Analyze local binaries with Better Binary and Hopper through its MCP session workflow. description: Analyze local binaries with REA and Hopper through its MCP session workflow.
--- ---
# Better Binary Analysis # REA Analysis
Call `open_binary` with a readable local path before analysis. Begin with `binary_overview`, use strings and symbol searches to identify relevant procedures, then use `procedure_pseudo_code`, callers, callees, and xrefs to build evidence. Call `open_binary` again to switch targets and `close_binary` when finished. Never claim real-Hopper behavior from mock-only evidence. Call `open_binary` with a readable local path before analysis. Begin with `binary_overview`, use strings and symbol searches to identify relevant procedures, then use `procedure_pseudo_code`, callers, callees, and xrefs to build evidence. Call `open_binary` again to switch targets and `close_binary` when finished. Never claim real-Hopper behavior from mock-only evidence.

View File

@ -115,8 +115,7 @@ export const runSetup = async (
...(doctor.healthy ...(doctor.healthy
? {} ? {}
: { : {
remediation: remediation: "Run rea doctor and apply each reported remediation.",
"Run better-binary doctor and apply each reported remediation.",
}), }),
}; };
}; };
@ -186,7 +185,10 @@ export const configureJsonClient = async (
if (!isMissing(cause)) return { status: "failed", reason: "readback" }; if (!isMissing(cause)) return { status: "failed", reason: "readback" };
} }
const servers = objectValue(document.mcpServers); const servers = objectValue(document.mcpServers);
const desired = { command: PRODUCT_IDENTITY.mcpBinary }; const desired = {
command: "npx",
args: ["-y", PRODUCT_IDENTITY.packageName, "mcp"],
};
if ( if (
JSON.stringify(servers[PRODUCT_IDENTITY.mcpServerKey]) === JSON.stringify(servers[PRODUCT_IDENTITY.mcpServerKey]) ===
JSON.stringify(desired) JSON.stringify(desired)
@ -194,7 +196,7 @@ export const configureJsonClient = async (
return { status: "unchanged" }; return { status: "unchanged" };
let backupPath: string | undefined; let backupPath: string | undefined;
if (original !== undefined) { if (original !== undefined) {
backupPath = `${client.configPath}.better-binary.backup`; backupPath = `${client.configPath}.rea.backup`;
try { try {
await copyFile(client.configPath, backupPath); await copyFile(client.configPath, backupPath);
} catch { } catch {
@ -206,7 +208,7 @@ export const configureJsonClient = async (
[PRODUCT_IDENTITY.mcpServerKey]: desired, [PRODUCT_IDENTITY.mcpServerKey]: desired,
}; };
const encoded = `${JSON.stringify(document, null, 2)}\n`; const encoded = `${JSON.stringify(document, null, 2)}\n`;
const temporary = `${client.configPath}.better-binary.tmp`; const temporary = `${client.configPath}.rea.tmp`;
try { try {
await mkdir(dirname(client.configPath), { recursive: true }); await mkdir(dirname(client.configPath), { recursive: true });
await writeFile(temporary, encoded, { encoding: "utf8", mode: 0o600 }); await writeFile(temporary, encoded, { encoding: "utf8", mode: 0o600 });
@ -243,7 +245,7 @@ const installCanonicalSkill = async (
PRODUCT_IDENTITY.skillName, PRODUCT_IDENTITY.skillName,
"SKILL.md", "SKILL.md",
); );
const content = `---\nname: ${PRODUCT_IDENTITY.skillName}\ndescription: Analyze binaries with Better Binary and Hopper.\n---\n\nOpen a target with open_binary, begin with binary_overview, and close it when finished.\n`; const content = `---\nname: ${PRODUCT_IDENTITY.skillName}\ndescription: Analyze binaries with REA and Hopper.\n---\n\nOpen a target with open_binary, begin with binary_overview, and close it when finished.\n`;
try { try {
if ( if (
(await readFile(destination, "utf8").catch(() => undefined)) === content (await readFile(destination, "utf8").catch(() => undefined)) === content

View File

@ -180,7 +180,7 @@ export const OFFICIAL_TOOL_CONTRACTS = [
), ),
] as const satisfies readonly ToolContract[]; ] as const satisfies readonly ToolContract[];
/** All 8 reverse-engineering tools implemented by betterBinaryMCP. */ /** All 8 reverse-engineering tools implemented by REA. */
export const ENHANCED_TOOL_CONTRACTS = [ export const ENHANCED_TOOL_CONTRACTS = [
enhanced( enhanced(
"swift_classes", "swift_classes",

View File

@ -41,8 +41,8 @@ export class HopperApplicationLauncher implements BridgeLauncher {
): Promise<Result<BridgeLaunch, HopperStartError>> { ): Promise<Result<BridgeLaunch, HopperStartError>> {
const bootstrapPath = `${session.directory}/bootstrap.py`; const bootstrapPath = `${session.directory}/bootstrap.py`;
const source = [ const source = [
`BETTER_BINARY_SOCKET = ${JSON.stringify(session.socketPath)}`, `REA_SOCKET = ${JSON.stringify(session.socketPath)}`,
`BETTER_BINARY_TOKEN = ${JSON.stringify(session.token)}`, `REA_TOKEN = ${JSON.stringify(session.token)}`,
`exec(compile(open(${JSON.stringify(this.options.bridgeScriptPath)}, 'rb').read(), ${JSON.stringify(this.options.bridgeScriptPath)}, 'exec'))`, `exec(compile(open(${JSON.stringify(this.options.bridgeScriptPath)}, 'rb').read(), ${JSON.stringify(this.options.bridgeScriptPath)}, 'exec'))`,
"", "",
].join("\n"); ].join("\n");

View File

@ -341,7 +341,7 @@ const parseServerInfo = (
typeof value === "object" && typeof value === "object" &&
value !== null && value !== null &&
!Array.isArray(value) && !Array.isArray(value) &&
value.name === "betterBinaryMCP Hopper bridge" && value.name === "REA Hopper bridge" &&
typeof value.version === "string" typeof value.version === "string"
) { ) {
return ok({ name: value.name, version: value.version }); return ok({ name: value.name, version: value.version });

View File

@ -1,10 +1,10 @@
/** Provisional product identifiers kept together for release-time renaming. */ /** Public REA identifiers kept together for consistent packaging and setup. */
export const PRODUCT_IDENTITY = { export const PRODUCT_IDENTITY = {
displayName: "Better Binary", displayName: "REA",
packageName: "better-binary-mcp", packageName: "@morluto/rea",
cliBinary: "better-binary", cliBinary: "rea",
mcpBinary: "better-binary-mcp", mcpCommand: "npx -y @morluto/rea mcp",
mcpServerKey: "better-binary", mcpServerKey: "rea",
skillName: "better-binary-analysis", skillName: "rea-analysis",
configFileName: "better-binary.json", configFileName: "rea.json",
} as const; } as const;

View File

@ -24,7 +24,7 @@ const server = createServer((socket) => {
send( send(
{ {
id: request.id, id: request.id,
result: { name: "betterBinaryMCP Hopper bridge", version: "1.0.0" }, result: { name: "REA Hopper bridge", version: "1.0.0" },
}, },
true, true,
); );

View File

@ -51,7 +51,7 @@ const startClient = async () => {
clients.push(client); clients.push(client);
await expect(client.start()).resolves.toEqual({ await expect(client.start()).resolves.toEqual({
ok: true, ok: true,
value: { name: "betterBinaryMCP Hopper bridge", version: "1.0.0" }, value: { name: "REA Hopper bridge", version: "1.0.0" },
}); });
return client; return client;
}; };

24
tests/identity.test.ts Normal file
View File

@ -0,0 +1,24 @@
import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { PRODUCT_IDENTITY } from "../src/identity.js";
describe("package identity", () => {
it("keeps the manifest, executable, and floating MCP command aligned", async () => {
const packageJson = z
.object({ name: z.string(), bin: z.record(z.string(), z.string()) })
.parse(
JSON.parse(
await readFile(
fileURLToPath(new URL("../package.json", import.meta.url)),
"utf8",
),
),
);
expect(packageJson.name).toBe(PRODUCT_IDENTITY.packageName);
expect(Object.keys(packageJson.bin)).toEqual([PRODUCT_IDENTITY.cliBinary]);
expect(PRODUCT_IDENTITY.mcpCommand).toBe(`npx -y ${packageJson.name} mcp`);
});
});

View File

@ -14,42 +14,43 @@ afterEach(async () => {
describe("JSON client configuration transaction", () => { describe("JSON client configuration transaction", () => {
it("preserves existing keys, creates a backup, and reads back the MCP entry", async () => { it("preserves existing keys, creates a backup, and reads back the MCP entry", async () => {
directory = await mkdtemp(join(tmpdir(), "bb-setup-")); directory = await mkdtemp(join(tmpdir(), "rea-setup-"));
const configPath = join(directory, "mcp.json"); const configPath = join(directory, "mcp.json");
const original = const original =
'{"theme":"dark","mcpServers":{"other":{"command":"other"}}}\n'; '{"theme":"dark","mcpServers":{"other":{"command":"other"}}}\n';
await writeFile(configPath, original); await writeFile(configPath, original);
const result = await configureJsonClient({ name: "cursor", configPath }); const result = await configureJsonClient({ name: "cursor", configPath });
expect(result.status).toBe("configured"); expect(result.status).toBe("configured");
expect(await readFile(`${configPath}.better-binary.backup`, "utf8")).toBe( expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original);
original,
);
expect(JSON.parse(await readFile(configPath, "utf8"))).toMatchObject({ expect(JSON.parse(await readFile(configPath, "utf8"))).toMatchObject({
theme: "dark", theme: "dark",
mcpServers: { mcpServers: {
other: { command: "other" }, other: { command: "other" },
"better-binary": { command: "better-binary-mcp" }, rea: {
command: "npx",
args: ["-y", "@morluto/rea", "mcp"],
},
}, },
}); });
}); });
it("performs no write or second backup when configuration already matches", async () => { it("performs no write or second backup when configuration already matches", async () => {
directory = await mkdtemp(join(tmpdir(), "bb-setup-")); directory = await mkdtemp(join(tmpdir(), "rea-setup-"));
const configPath = join(directory, "mcp.json"); const configPath = join(directory, "mcp.json");
await writeFile( await writeFile(
configPath, configPath,
'{"mcpServers":{"better-binary":{"command":"better-binary-mcp"}}}\n', '{"mcpServers":{"rea":{"command":"npx","args":["-y","@morluto/rea","mcp"]}}}\n',
); );
expect(await configureJsonClient({ name: "cursor", configPath })).toEqual({ expect(await configureJsonClient({ name: "cursor", configPath })).toEqual({
status: "unchanged", status: "unchanged",
}); });
await expect( await expect(
readFile(`${configPath}.better-binary.backup`, "utf8"), readFile(`${configPath}.rea.backup`, "utf8"),
).rejects.toThrow(); ).rejects.toThrow();
}); });
it("refuses malformed existing JSON without overwriting it", async () => { it("refuses malformed existing JSON without overwriting it", async () => {
directory = await mkdtemp(join(tmpdir(), "bb-setup-")); directory = await mkdtemp(join(tmpdir(), "rea-setup-"));
const configPath = join(directory, "mcp.json"); const configPath = join(directory, "mcp.json");
await writeFile(configPath, "not-json"); await writeFile(configPath, "not-json");
expect(await configureJsonClient({ name: "cursor", configPath })).toEqual({ expect(await configureJsonClient({ name: "cursor", configPath })).toEqual({

View File

@ -13,7 +13,7 @@
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"outDir": "dist", "outDir": "dist",
"rootDir": ".", "rootDir": ".",
"skipLibCheck": false, "skipLibCheck": true,
"strict": true, "strict": true,
"target": "ES2023", "target": "ES2023",
"types": ["node"], "types": ["node"],