refactor(cli): share runtime between CLI and MCP
This commit is contained in:
parent
d9c9025ffb
commit
97541d8dad
|
|
@ -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();
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
@ -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<typeof Cli.create> => {
|
||||
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;
|
||||
};
|
||||
25
src/main.ts
25
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<number> => {
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue