diff --git a/README.md b/README.md index 61357610c..0521af8d1 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bridge/hopper_bridge.py b/bridge/hopper_bridge.py index d55b89ca9..529db542f 100644 --- a/bridge/hopper_bridge.py +++ b/bridge/hopper_bridge.py @@ -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. diff --git a/package-lock.json b/package-lock.json index d9f718a1b..9bdcbd6d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index b6accb4ac..b7763b33a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/print-mcp-config.mjs b/scripts/print-mcp-config.mjs index 29cd2d939..08574516c 100755 --- a/scripts/print-mcp-config.mjs +++ b/scripts/print-mcp-config.mjs @@ -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, }, }, diff --git a/scripts/verify-package.mjs b/scripts/verify-package.mjs index c410ec009..d94ae02b9 100644 --- a/scripts/verify-package.mjs +++ b/scripts/verify-package.mjs @@ -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 }); diff --git a/skills/better-binary-analysis/SKILL.md b/skills/rea-analysis/SKILL.md similarity index 71% rename from skills/better-binary-analysis/SKILL.md rename to skills/rea-analysis/SKILL.md index fa6c675ec..98a3de2ac 100644 --- a/skills/better-binary-analysis/SKILL.md +++ b/skills/rea-analysis/SKILL.md @@ -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. diff --git a/src/application/Setup.ts b/src/application/Setup.ts index 2f35b41d3..da015d6ac 100644 --- a/src/application/Setup.ts +++ b/src/application/Setup.ts @@ -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 diff --git a/src/contracts/toolContracts.ts b/src/contracts/toolContracts.ts index e43bb09f0..da975c070 100644 --- a/src/contracts/toolContracts.ts +++ b/src/contracts/toolContracts.ts @@ -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", diff --git a/src/hopper/BridgeLauncher.ts b/src/hopper/BridgeLauncher.ts index 8b34bf81a..74c6a4cf3 100644 --- a/src/hopper/BridgeLauncher.ts +++ b/src/hopper/BridgeLauncher.ts @@ -41,8 +41,8 @@ export class HopperApplicationLauncher implements BridgeLauncher { ): Promise> { 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"); diff --git a/src/hopper/HopperClient.ts b/src/hopper/HopperClient.ts index 4b54681be..772331a16 100644 --- a/src/hopper/HopperClient.ts +++ b/src/hopper/HopperClient.ts @@ -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 }); diff --git a/src/identity.ts b/src/identity.ts index 15881dfda..ba92996c9 100644 --- a/src/identity.ts +++ b/src/identity.ts @@ -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; diff --git a/tests/fixtures/fakeHopper.mjs b/tests/fixtures/fakeHopper.mjs index f5c034bf3..64cfa4121 100644 --- a/tests/fixtures/fakeHopper.mjs +++ b/tests/fixtures/fakeHopper.mjs @@ -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, ); diff --git a/tests/hopperClient.test.ts b/tests/hopperClient.test.ts index 111598739..d43061099 100644 --- a/tests/hopperClient.test.ts +++ b/tests/hopperClient.test.ts @@ -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; }; diff --git a/tests/identity.test.ts b/tests/identity.test.ts new file mode 100644 index 000000000..01196cb01 --- /dev/null +++ b/tests/identity.test.ts @@ -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`); + }); +}); diff --git a/tests/setupConfig.test.ts b/tests/setupConfig.test.ts index 1154f74cd..f62384683 100644 --- a/tests/setupConfig.test.ts +++ b/tests/setupConfig.test.ts @@ -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({ diff --git a/tsconfig.json b/tsconfig.json index 2c83189ca..da6b70239 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "noUncheckedIndexedAccess": true, "outDir": "dist", "rootDir": ".", - "skipLibCheck": false, + "skipLibCheck": true, "strict": true, "target": "ES2023", "types": ["node"],