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
@ -10,24 +10,26 @@ Requires macOS 12+ and Node.js 22+.
npm ci
npm run build
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.
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:
```bash
better-binary --help
better-binary --llms
better-binary doctor --target /path/to/binary
better-binary skills add
better-binary mcp add
rea --help
rea --llms
rea doctor --target /path/to/binary
rea skills add
rea mcp add
```
## 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.
2. Call `binary_overview`, then use decompilation, strings, symbols, and xrefs tools.
@ -43,8 +45,9 @@ Manual target-free MCP configuration:
```json
{
"mcpServers": {
"better-binary": {
"command": "better-binary-mcp"
"rea": {
"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
npm run check
npm pack --dry-run
npm run verify:package
HOPPER_TARGET_PATH=/path/to/target-a \
HOPPER_SECOND_TARGET_PATH=/path/to/distinct-target-b \
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.
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

View File

@ -119,7 +119,7 @@ def _assembly(procedure):
def _dispatch(method, params):
global _selected_document
if method == "health":
return {"name": "betterBinaryMCP Hopper bridge", "version": "1.0.0"}
return {"name": "REA Hopper bridge", "version": "1.0.0"}
if method == "shutdown":
return {"shutdown": True}
if method == "list_documents":
@ -239,7 +239,7 @@ def _serve_connection(connection):
if set(request) != {"id", "token", "method", "params"}:
raise ValueError("Invalid bridge request shape")
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")
result = _dispatch(request["method"], request["params"])
should_stop = request["method"] == "shutdown"
@ -255,19 +255,19 @@ def _serve_connection(connection):
def _run():
if os.path.exists(BETTER_BINARY_SOCKET):
os.unlink(BETTER_BINARY_SOCKET)
if os.path.exists(REA_SOCKET):
os.unlink(REA_SOCKET)
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(BETTER_BINARY_SOCKET)
os.chmod(BETTER_BINARY_SOCKET, 0o600)
server.bind(REA_SOCKET)
os.chmod(REA_SOCKET, 0o600)
server.listen(1)
try:
connection, _ = server.accept()
_serve_connection(connection)
finally:
server.close()
if os.path.exists(BETTER_BINARY_SOCKET):
os.unlink(BETTER_BINARY_SOCKET)
if os.path.exists(REA_SOCKET):
os.unlink(REA_SOCKET)
# 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",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "better-binary-mcp",
"name": "@morluto/rea",
"version": "0.1.0",
"dependencies": {
"@modelcontextprotocol/server": "2.0.0-beta.3",
@ -13,7 +13,7 @@
"zod": "4.4.3"
},
"bin": {
"better-binary-mcp": "dist/main.js"
"rea": "scripts/rea.mjs"
},
"devDependencies": {
"@eslint/js": "9.39.2",

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
---
name: better-binary-analysis
description: Analyze local binaries with Better Binary and Hopper through its MCP session workflow.
name: rea-analysis
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.

View File

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

View File

@ -180,7 +180,7 @@ export const OFFICIAL_TOOL_CONTRACTS = [
),
] 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 = [
enhanced(
"swift_classes",

View File

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

View File

@ -341,7 +341,7 @@ const parseServerInfo = (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
value.name === "betterBinaryMCP Hopper bridge" &&
value.name === "REA Hopper bridge" &&
typeof value.version === "string"
) {
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 = {
displayName: "Better Binary",
packageName: "better-binary-mcp",
cliBinary: "better-binary",
mcpBinary: "better-binary-mcp",
mcpServerKey: "better-binary",
skillName: "better-binary-analysis",
configFileName: "better-binary.json",
displayName: "REA",
packageName: "@morluto/rea",
cliBinary: "rea",
mcpCommand: "npx -y @morluto/rea mcp",
mcpServerKey: "rea",
skillName: "rea-analysis",
configFileName: "rea.json",
} as const;

View File

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

View File

@ -51,7 +51,7 @@ const startClient = async () => {
clients.push(client);
await expect(client.start()).resolves.toEqual({
ok: true,
value: { name: "betterBinaryMCP Hopper bridge", version: "1.0.0" },
value: { name: "REA Hopper bridge", version: "1.0.0" },
});
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", () => {
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 original =
'{"theme":"dark","mcpServers":{"other":{"command":"other"}}}\n';
await writeFile(configPath, original);
const result = await configureJsonClient({ name: "cursor", configPath });
expect(result.status).toBe("configured");
expect(await readFile(`${configPath}.better-binary.backup`, "utf8")).toBe(
original,
);
expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original);
expect(JSON.parse(await readFile(configPath, "utf8"))).toMatchObject({
theme: "dark",
mcpServers: {
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 () => {
directory = await mkdtemp(join(tmpdir(), "bb-setup-"));
directory = await mkdtemp(join(tmpdir(), "rea-setup-"));
const configPath = join(directory, "mcp.json");
await writeFile(
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({
status: "unchanged",
});
await expect(
readFile(`${configPath}.better-binary.backup`, "utf8"),
readFile(`${configPath}.rea.backup`, "utf8"),
).rejects.toThrow();
});
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");
await writeFile(configPath, "not-json");
expect(await configureJsonClient({ name: "cursor", configPath })).toEqual({

View File

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