diff --git a/scripts/better-binary.mjs b/scripts/better-binary.mjs deleted file mode 100644 index f621b18c6..000000000 --- a/scripts/better-binary.mjs +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env node - -import { Cli, z } from "incur"; -import { runDoctor } from "../dist/application/Doctor.js"; -import { runDirectAnalysis } from "../dist/application/DirectAnalysis.js"; -import { runSetup } from "../dist/application/Setup.js"; -import { PRODUCT_IDENTITY } from "../dist/identity.js"; - -const cli = Cli.create(PRODUCT_IDENTITY.cliBinary, { - version: "0.1.0", - description: "Install, diagnose, and automate Hopper binary analysis.", - mcp: { - command: PRODUCT_IDENTITY.mcpBinary, - instructions: - "Open a local target with open_binary, analyze it, and close the session when finished.", - }, - sync: { - cwd: import.meta.dirname, - include: ["../skills/*"], - suggestions: [ - "open a binary and summarize it", - "decompile a procedure", - "diagnose my Hopper setup", - ], - }, -}); - -cli.command("setup", { - description: "Install prerequisites and configure binary analysis", - options: z.object({ - yes: z - .boolean() - .default(false) - .describe("Approve prerequisite installation"), - }), - alias: { yes: "y" }, - run: ({ options }) => runSetup(options.yes), -}); -cli.command("doctor", { - description: "Diagnose the local Hopper integration", - options: z.object({ - target: z.string().optional().describe("Optional binary path to check"), - }), - run: ({ options }) => runDoctor(options.target), -}); -cli.command("analyze", { - description: "Analyze a binary through the beta.3 MCP server", - args: z.object({ - path: z.string().describe("Binary or Hopper database path"), - }), - run: ({ args }) => runDirectAnalysis(args.path, "binary_overview", {}), -}); -cli.command("decompile", { - description: "Decompile an address through the beta.3 MCP server", - args: z.object({ - path: z.string().describe("Binary path"), - address: z.string().describe("Procedure address"), - }), - run: ({ args }) => - runDirectAnalysis(args.path, "procedure_pseudo_code", { - procedure: args.address, - }), -}); -cli.serve(); diff --git a/scripts/rea.mjs b/scripts/rea.mjs new file mode 100644 index 000000000..2e41bd13d --- /dev/null +++ b/scripts/rea.mjs @@ -0,0 +1,13 @@ +#!/usr/bin/env node + +const args = process.argv.slice(2); +const isMcpMode = + args[0] === "--mcp" || (args.length === 1 && args[0] === "mcp"); + +if (isMcpMode) { + const { run } = await import("../dist/main.js"); + process.exitCode = await run(); +} else { + const { createCli } = await import("../dist/cli.js"); + await createCli().serve(args); +} diff --git a/src/application/DirectAnalysis.ts b/src/application/DirectAnalysis.ts index c8d23c00e..68a1e0753 100644 --- a/src/application/DirectAnalysis.ts +++ b/src/application/DirectAnalysis.ts @@ -1,10 +1,6 @@ -import { fileURLToPath } from "node:url"; - import { parseConfig } from "../config.js"; -import { HopperApplicationLauncher } from "../hopper/BridgeLauncher.js"; -import { HopperClient } from "../hopper/HopperClient.js"; import type { JsonValue } from "../hopper/protocol.js"; -import { BinarySession } from "./BinarySession.js"; +import { createBinarySession } from "./runtime.js"; /** Open one binary, execute one tool, and always release the target session. */ export const runDirectAnalysis = async ( @@ -15,24 +11,7 @@ export const runDirectAnalysis = async ( const config = parseConfig(process.env); if (!config.ok) return { error: config.error._tag, message: config.error.message }; - const bridgeScriptPath = fileURLToPath( - new URL("../../bridge/hopper_bridge.py", import.meta.url), - ); - const session = new BinarySession( - (target) => - new HopperClient({ - launcher: new HopperApplicationLauncher({ - launcherPath: config.value.hopperLauncherPath, - targetPath: target.path, - targetKind: target.kind, - loaderArgs: - config.value.hopperLoaderArgs.length > 0 - ? config.value.hopperLoaderArgs - : target.loaderArgs, - bridgeScriptPath, - }), - }), - ); + const session = createBinarySession(config.value); try { const opened = await session.open(path); if (!opened.ok) diff --git a/src/application/runtime.ts b/src/application/runtime.ts new file mode 100644 index 000000000..fbd3381fa --- /dev/null +++ b/src/application/runtime.ts @@ -0,0 +1,28 @@ +import { fileURLToPath } from "node:url"; + +import type { AppConfig } from "../config.js"; +import { BinarySession } from "./BinarySession.js"; +import { HopperApplicationLauncher } from "../hopper/BridgeLauncher.js"; +import { HopperClient } from "../hopper/HopperClient.js"; + +/** Create the target-switching session used by both the CLI and MCP adapters. */ +export const createBinarySession = (config: AppConfig): BinarySession => { + const bridgeScriptPath = fileURLToPath( + new URL("../../bridge/hopper_bridge.py", import.meta.url), + ); + return new BinarySession( + (target) => + new HopperClient({ + launcher: new HopperApplicationLauncher({ + launcherPath: config.hopperLauncherPath, + targetPath: target.path, + targetKind: target.kind, + loaderArgs: + config.hopperLoaderArgs.length > 0 + ? config.hopperLoaderArgs + : target.loaderArgs, + bridgeScriptPath, + }), + }), + ); +}; diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 000000000..03cc0c3dc --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,67 @@ +import { Cli, z } from "incur"; +import { fileURLToPath } from "node:url"; + +import { runDoctor } from "./application/Doctor.js"; +import { runDirectAnalysis } from "./application/DirectAnalysis.js"; +import { runSetup } from "./application/Setup.js"; +import { PRODUCT_IDENTITY } from "./identity.js"; + +/** Build the Incur command tree for the non-MCP CLI commands. */ +export const createCli = (): ReturnType => { + const cli = Cli.create(PRODUCT_IDENTITY.cliBinary, { + version: "0.1.0", + description: "Install, diagnose, and automate Hopper binary analysis.", + mcp: { + command: PRODUCT_IDENTITY.mcpCommand, + instructions: + "Open a local target with open_binary, analyze it, and close the session when finished.", + }, + sync: { + cwd: fileURLToPath(new URL("..", import.meta.url)), + include: ["skills/*"], + suggestions: [ + "open a binary and summarize it", + "decompile a procedure", + "diagnose my Hopper setup", + ], + }, + }); + + cli.command("setup", { + description: "Install prerequisites and configure binary analysis", + options: z.object({ + yes: z + .boolean() + .default(false) + .describe("Approve prerequisite installation"), + }), + alias: { yes: "y" }, + run: ({ options }) => runSetup(options.yes), + }); + cli.command("doctor", { + description: "Diagnose the local Hopper integration", + options: z.object({ + target: z.string().optional().describe("Optional binary path to check"), + }), + run: ({ options }) => runDoctor(options.target), + }); + cli.command("analyze", { + description: "Analyze a binary through the shared Hopper core", + args: z.object({ + path: z.string().describe("Binary or Hopper database path"), + }), + run: ({ args }) => runDirectAnalysis(args.path, "binary_overview", {}), + }); + cli.command("decompile", { + description: "Decompile an address through the shared Hopper core", + args: z.object({ + path: z.string().describe("Binary path"), + address: z.string().describe("Procedure address"), + }), + run: ({ args }) => + runDirectAnalysis(args.path, "procedure_pseudo_code", { + procedure: args.address, + }), + }); + return cli; +}; diff --git a/src/main.ts b/src/main.ts index c9b73111b..e2b801433 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,6 @@ #!/usr/bin/env node import { pathToFileURL } from "node:url"; -import { fileURLToPath } from "node:url"; import { realpathSync } from "node:fs"; import { @@ -10,9 +9,7 @@ import { } from "@modelcontextprotocol/server/stdio"; import { parseConfig } from "./config.js"; -import { BinarySession } from "./application/BinarySession.js"; -import { HopperApplicationLauncher } from "./hopper/BridgeLauncher.js"; -import { HopperClient } from "./hopper/HopperClient.js"; +import { createBinarySession } from "./application/runtime.js"; import { createServer } from "./server/createServer.js"; /** Start the production stdio server and install owned shutdown handlers. */ @@ -23,27 +20,11 @@ export const run = async (): Promise => { return 1; } - const bridgeScriptPath = fileURLToPath( - new URL("../bridge/hopper_bridge.py", import.meta.url), - ); - const session = new BinarySession( - (target) => - new HopperClient({ - launcher: new HopperApplicationLauncher({ - launcherPath: config.value.hopperLauncherPath, - targetPath: target.path, - targetKind: target.kind, - loaderArgs: - config.value.hopperLoaderArgs.length > 0 - ? config.value.hopperLoaderArgs - : target.loaderArgs, - bridgeScriptPath, - }), - }), - ); + const session = createBinarySession(config.value); if (config.value.hopperTargetPath !== undefined) { const opened = await session.open(config.value.hopperTargetPath); if (!opened.ok) { + await session.close(); process.stderr.write(`${opened.error._tag}: ${opened.error.message}\n`); return 1; }