feat(identity): rename package and CLI to REA
This commit is contained in:
parent
97541d8dad
commit
3d70812c37
35
README.md
35
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
|
## 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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"],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue