rea-agents 0.5.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,30 +1,30 @@
1
1
  import { access, copyFile, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join, resolve } from "node:path";
4
- import { execFile } from "node:child_process";
5
- import { promisify } from "node:util";
6
4
  import { z } from "zod";
7
5
  import writeFileAtomic from "write-file-atomic";
8
6
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
9
7
  import { PRODUCT_IDENTITY } from "../identity.js";
8
+ import { supportsNodeVersion } from "../domain/runtimeVersion.js";
10
9
  import { runDoctor, systemDoctorHost } from "./Doctor.js";
11
- import { probeHomebrew } from "./homebrew.js";
12
10
  import { installLinuxHopper, readLinuxDistribution, } from "./LinuxHopper.js";
13
- const execFileAsync = promisify(execFile);
11
+ import { installMacHopper } from "./MacHopper.js";
14
12
  const registrationCommand = () => process.env.npm_command === "exec"
15
13
  ? ["npx", "-y", PRODUCT_IDENTITY.packageName, "mcp"]
16
14
  : [resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), "mcp"];
17
15
  /**
18
16
  * Install prerequisites and configure detected clients idempotently.
19
- * With `yes` false, mutations requiring consent are reported as `needs_human`;
20
- * with `yes` true, external installers still fail closed if they require UI.
17
+ * Discovery always precedes mutation. Interactive confirmation or explicit
18
+ * unattended options authorize only the actions represented in the result.
21
19
  */
22
- export const runSetup = async (yes, host = systemSetupHost()) => {
23
- const actions = [];
20
+ export const runSetup = async (options, host = systemSetupHost(), confirm) => {
21
+ const appliedActions = [];
22
+ let plannedActions = [];
24
23
  const clients = {};
25
24
  const fail = async (remediation) => ({
26
25
  status: "needs_human",
27
- actions,
26
+ plannedActions,
27
+ appliedActions,
28
28
  clients,
29
29
  doctor: await host.doctor(),
30
30
  remediation,
@@ -32,43 +32,85 @@ export const runSetup = async (yes, host = systemSetupHost()) => {
32
32
  const unsupported = await hostRemediation(host);
33
33
  if (unsupported !== undefined)
34
34
  return fail(unsupported);
35
- if (host.platform === "darwin" && !(await host.hasHomebrew())) {
36
- if (!yes)
37
- return fail("Re-run with --yes to install Homebrew.");
38
- if (!(await host.installHomebrew()))
39
- return fail("Homebrew installation was interrupted; resolve the installer prompt and re-run setup.");
40
- actions.push("installed_homebrew");
41
- }
42
35
  let hopperPath = await host.hopperPath();
43
- if (hopperPath === undefined) {
44
- if (!yes)
45
- return fail("Re-run with --yes to install Hopper.");
46
- if (!(await host.installHopper()))
47
- return fail("Hopper installation was interrupted; resolve the system prompt and re-run setup.");
48
- actions.push("installed_hopper");
49
- hopperPath = await host.hopperPath();
36
+ const detectedClients = await host.detectedClients();
37
+ plannedActions = [
38
+ ...(hopperPath === undefined
39
+ ? [
40
+ {
41
+ kind: "install_hopper",
42
+ target: host.platform === "darwin"
43
+ ? "~/Applications/Hopper Disassembler.app"
44
+ : "system package manager",
45
+ detail: "Download the official Hopper package, verify it, install it, and open Hopper for activation.",
46
+ external: true,
47
+ },
48
+ ]
49
+ : []),
50
+ ...detectedClients
51
+ .filter(({ format }) => format !== "unsupported")
52
+ .map((client) => ({
53
+ kind: "configure_client",
54
+ target: client.configPath,
55
+ detail: `Add the REA MCP registration for ${client.name}; preserve unrelated configuration.`,
56
+ external: false,
57
+ })),
58
+ {
59
+ kind: "install_skill",
60
+ target: "~/.agents/skills/rea-analysis/SKILL.md",
61
+ detail: "Install or update the bundled REA analysis skill.",
62
+ external: false,
63
+ },
64
+ ];
65
+ let approved = options.approved;
66
+ let interactiveApproval = false;
67
+ if (!approved && confirm !== undefined && !options.structured) {
68
+ interactiveApproval = await confirm(plannedActions);
69
+ approved = interactiveApproval;
70
+ }
71
+ if (!approved)
72
+ return {
73
+ status: confirm === undefined ? "needs_confirmation" : "planned",
74
+ plannedActions,
75
+ appliedActions,
76
+ clients,
77
+ doctor: await host.doctor(),
78
+ remediation: "Review the setup plan, then rerun interactively or with --yes.",
79
+ };
80
+ if (hopperPath === undefined &&
81
+ (interactiveApproval || options.installHopper)) {
82
+ hopperPath = await host.installHopper();
50
83
  if (hopperPath === undefined)
51
- return fail("Hopper installation completed but its launcher was not found.");
84
+ return {
85
+ status: "needs_human",
86
+ plannedActions,
87
+ appliedActions,
88
+ clients,
89
+ doctor: await host.doctor(),
90
+ remediation: "Hopper installation failed; install Hopper manually or rerun setup after resolving the reported system error.",
91
+ };
92
+ appliedActions.push("installed_hopper");
52
93
  }
53
- for (const client of await host.detectedClients()) {
94
+ for (const client of detectedClients) {
54
95
  const result = await host.configureClient(client, hopperPath, registrationCommand());
55
96
  clients[client.name] = result;
56
97
  if (result.status === "failed")
57
98
  return fail(`${client.name} configuration ${result.reason} verification failed; no successful configuration was reported.`);
58
99
  if (result.status === "configured")
59
- actions.push(`configured_${client.name}`);
100
+ appliedActions.push(`configured_${client.name}`);
60
101
  }
61
102
  const skill = await host.installSkill();
62
103
  if (skill === "failed")
63
104
  return fail("Agent skill installation or readback failed.");
64
105
  if (skill === "installed")
65
- actions.push("installed_skill");
106
+ appliedActions.push("installed_skill");
66
107
  const doctor = await host.doctor();
67
- const activationRequired = actions.includes("installed_hopper");
108
+ const activationRequired = appliedActions.includes("installed_hopper");
68
109
  const ready = doctor.healthy && !activationRequired;
69
110
  return {
70
111
  status: ready ? "ready" : "needs_human",
71
- actions,
112
+ plannedActions,
113
+ appliedActions,
72
114
  clients,
73
115
  doctor,
74
116
  ...(ready
@@ -76,15 +118,17 @@ export const runSetup = async (yes, host = systemSetupHost()) => {
76
118
  : {
77
119
  remediation: activationRequired
78
120
  ? "Open Hopper, complete its one-time activation, then rerun rea doctor --json."
79
- : "Run rea doctor and apply each reported remediation.",
121
+ : hopperPath === undefined
122
+ ? "Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis."
123
+ : "Run rea doctor and apply each reported remediation.",
80
124
  }),
81
125
  };
82
126
  };
83
127
  const hostRemediation = async (host) => {
84
128
  if (host.platform !== "darwin" && host.platform !== "linux")
85
129
  return "REA supports Hopper on macOS and selected 64-bit Linux distributions.";
86
- if (major(host.nodeVersion) < 24)
87
- return "Install Node.js 24.18 or newer.";
130
+ if (!supportsNodeVersion(host.nodeVersion))
131
+ return "Install Node.js 22.19+ or 24.11+ and rerun setup.";
88
132
  if (host.platform === "darwin") {
89
133
  const version = await host.macosVersion();
90
134
  return version === undefined || major(version) < 12
@@ -95,7 +139,7 @@ const hostRemediation = async (host) => {
95
139
  ? undefined
96
140
  : "REA supports Hopper on Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.";
97
141
  };
98
- /** Production setup effects for macOS, Homebrew, JSON MCP clients, and the canonical skill directory. */
142
+ /** Production setup effects for Hopper, agent configuration, and the canonical skill directory. */
99
143
  const systemSetupHost = () => {
100
144
  const doctorHost = systemDoctorHost();
101
145
  return {
@@ -103,15 +147,13 @@ const systemSetupHost = () => {
103
147
  nodeVersion: process.versions.node,
104
148
  macosVersion: () => doctorHost.macosVersion(),
105
149
  linuxDistribution: readLinuxDistribution,
106
- hasHomebrew: () => brewSucceeds(["--version"]),
107
- installHomebrew: () => commandSucceeds("/bin/bash", [
108
- "-c",
109
- "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)",
110
- ], { ...process.env, NONINTERACTIVE: "1" }),
111
150
  hopperPath: async () => (await runDoctor(undefined, doctorHost)).hopperPath,
112
- installHopper: () => process.platform === "linux"
113
- ? installLinuxHopper().then(({ status }) => status === "installed")
114
- : brewSucceeds(["install", "--cask", "hopper-disassembler"]),
151
+ installHopper: async () => {
152
+ const result = process.platform === "linux"
153
+ ? await installLinuxHopper()
154
+ : await installMacHopper();
155
+ return result.status === "installed" ? result.launcherPath : undefined;
156
+ },
115
157
  detectedClients: () => detectClients(homedir()),
116
158
  configureClient: (client, hopperPath, command) => client.format === "unsupported"
117
159
  ? Promise.resolve({ status: "skipped" })
@@ -351,19 +393,6 @@ export const installCanonicalSkill = async (home) => {
351
393
  return "failed";
352
394
  }
353
395
  };
354
- const commandSucceeds = async (command, args, env) => {
355
- try {
356
- await execFileAsync(command, args, env === undefined ? {} : { env });
357
- return true;
358
- }
359
- catch {
360
- return false;
361
- }
362
- };
363
- const brewSucceeds = async (args) => {
364
- const result = await probeHomebrew(async (command) => (await commandSucceeds(command, args)) ? true : undefined);
365
- return result === true;
366
- };
367
396
  const major = (version) => Number.parseInt(version.split(".")[0] ?? "0", 10);
368
397
  const exists = async (path) => {
369
398
  try {
@@ -0,0 +1,106 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { realpath } from "node:fs/promises";
3
+ import { basename, dirname, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { promisify } from "node:util";
6
+ import { z } from "zod";
7
+ import { PRODUCT_IDENTITY } from "../identity.js";
8
+ const execFileAsync = promisify(execFile);
9
+ const registryResponseSchema = z.object({ version: z.string().min(1) });
10
+ const UPGRADE_COMMAND = "npm install --global rea-agents@latest";
11
+ /** Check the npm registry and update the same global REA installation. */
12
+ export const runUpgrade = async (currentVersion, host = systemUpgradeHost(), output = "human") => {
13
+ const latestVersion = await host.latestVersion();
14
+ if (latestVersion === currentVersion)
15
+ return {
16
+ status: "current",
17
+ currentVersion,
18
+ latestVersion,
19
+ installMethod: "npm",
20
+ };
21
+ const installation = await host.installation();
22
+ if (installation === undefined)
23
+ return {
24
+ status: "failed",
25
+ currentVersion,
26
+ latestVersion: latestVersion ?? null,
27
+ reason: "unknown-install-method",
28
+ remediation: `Update manually with: ${UPGRADE_COMMAND}`,
29
+ };
30
+ if (!(await host.installLatest(installation, output)))
31
+ return {
32
+ status: "failed",
33
+ currentVersion,
34
+ latestVersion: latestVersion ?? null,
35
+ reason: "install",
36
+ remediation: `${UPGRADE_COMMAND} failed.`,
37
+ };
38
+ return {
39
+ status: "upgraded",
40
+ previousVersion: currentVersion,
41
+ latestVersion: latestVersion ?? null,
42
+ versionCheck: latestVersion === undefined ? "unavailable" : "available",
43
+ installMethod: "npm",
44
+ command: UPGRADE_COMMAND,
45
+ };
46
+ };
47
+ /** Create the npm registry and subprocess effects for a production upgrade. */
48
+ export const systemUpgradeHost = () => ({
49
+ latestVersion: async () => {
50
+ try {
51
+ const response = await fetch(`https://registry.npmjs.org/${PRODUCT_IDENTITY.packageName}/latest`, { signal: AbortSignal.timeout(10_000) });
52
+ if (!response.ok)
53
+ return undefined;
54
+ const parsed = registryResponseSchema.safeParse(await response.json());
55
+ return parsed.success ? parsed.data.version : undefined;
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ },
61
+ installation: () => detectNpmInstallation(fileURLToPath(new URL("../..", import.meta.url)), systemNpmInstallationHost),
62
+ installLatest: ({ prefix }, output) => runCommand("npm", [
63
+ "install",
64
+ "--global",
65
+ "--prefix",
66
+ prefix,
67
+ `${PRODUCT_IDENTITY.packageName}@latest`,
68
+ ], output),
69
+ });
70
+ const systemNpmInstallationHost = {
71
+ canonicalPath: realpath,
72
+ globalRoot: async () => (await execFileAsync("npm", ["root", "--global"])).stdout.trim(),
73
+ globalPrefix: async () => (await execFileAsync("npm", ["prefix", "--global"])).stdout.trim(),
74
+ };
75
+ /** Identify the npm prefix only when it owns the running package. */
76
+ export const detectNpmInstallation = async (packageRoot, host) => {
77
+ try {
78
+ const canonicalPackageRoot = await host.canonicalPath(packageRoot);
79
+ const npmRoot = await host.globalRoot();
80
+ const globalPackageRoot = await host.canonicalPath(resolve(npmRoot, PRODUCT_IDENTITY.packageName));
81
+ if (canonicalPackageRoot === globalPackageRoot) {
82
+ const prefix = await host.globalPrefix();
83
+ return prefix.length === 0 ? undefined : { prefix };
84
+ }
85
+ return inferUnixGlobalPrefix(canonicalPackageRoot);
86
+ }
87
+ catch {
88
+ return inferUnixGlobalPrefix(packageRoot);
89
+ }
90
+ };
91
+ const inferUnixGlobalPrefix = (packageRoot) => {
92
+ const nodeModules = dirname(packageRoot);
93
+ const library = dirname(nodeModules);
94
+ if (basename(packageRoot) !== PRODUCT_IDENTITY.packageName ||
95
+ basename(nodeModules) !== "node_modules" ||
96
+ basename(library) !== "lib")
97
+ return undefined;
98
+ return { prefix: dirname(library) };
99
+ };
100
+ const runCommand = (command, arguments_, output) => new Promise((resolveResult) => {
101
+ const child = spawn(command, [...arguments_], {
102
+ stdio: output === "human" ? "inherit" : ["inherit", process.stderr, "inherit"],
103
+ });
104
+ child.once("error", () => resolveResult(false));
105
+ child.once("exit", (code) => resolveResult(code === 0));
106
+ });
package/dist/cli.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { Cli, z } from "incur";
2
2
  import { fileURLToPath } from "node:url";
3
+ import { createInterface } from "node:readline/promises";
3
4
  import { runDoctor } from "./application/Doctor.js";
4
5
  import { runDirectAnalysis, runProviderAnalysis, runSessionStatus, } from "./application/DirectAnalysis.js";
5
6
  import { runSetup } from "./application/Setup.js";
6
7
  import { runUninstall } from "./application/Uninstall.js";
8
+ import { runUpgrade, systemUpgradeHost } from "./application/Upgrade.js";
7
9
  import { PRODUCT_IDENTITY } from "./identity.js";
8
10
  import { createLogger, parseLogLevel } from "./logger.js";
9
11
  import { logCliCommand } from "./cliLogging.js";
@@ -59,10 +61,20 @@ const registerCoreCommands = (cli, logger) => {
59
61
  yes: z
60
62
  .boolean()
61
63
  .default(false)
62
- .describe("Approve prerequisite installation"),
64
+ .describe("Approve user-owned setup actions without prompting"),
65
+ installHopper: z
66
+ .boolean()
67
+ .default(false)
68
+ .describe("Also approve Hopper installation with --yes"),
63
69
  }),
64
- alias: { yes: "y" },
65
- run: ({ options }) => logCliCommand(logger, "setup", () => runSetup(options.yes)),
70
+ alias: { yes: "y", installHopper: "install-hopper" },
71
+ run: ({ options, formatExplicit }) => logCliCommand(logger, "setup", () => runSetup({
72
+ approved: options.yes,
73
+ installHopper: options.installHopper,
74
+ structured: formatExplicit,
75
+ }, undefined, options.yes || formatExplicit || process.stdin.isTTY !== true
76
+ ? undefined
77
+ : confirmSetup)),
66
78
  });
67
79
  cli.command("doctor", {
68
80
  description: "Check whether REA is ready",
@@ -82,6 +94,10 @@ const registerCoreCommands = (cli, logger) => {
82
94
  alias: { purgeData: "purge-data" },
83
95
  run: ({ options }) => logCliCommand(logger, "uninstall", () => runUninstall(options.purgeData)),
84
96
  });
97
+ cli.command("upgrade", {
98
+ description: "Upgrade a global npm installation to the latest REA release",
99
+ run: ({ formatExplicit }) => logCliCommand(logger, "upgrade", () => runUpgrade(process.env.REA_PACKAGE_VERSION ?? "0.0.0-development", systemUpgradeHost(), formatExplicit ? "structured" : "human")),
100
+ });
85
101
  cli.command("analyze", {
86
102
  description: "Get an overview of an app",
87
103
  args: z.object({
@@ -107,6 +123,24 @@ const registerCoreCommands = (cli, logger) => {
107
123
  run: ({ args }) => logCliCommand(logger, "decompile", () => runDirectAnalysis(args.path, "procedure_pseudo_code", { procedure: args.address }, logger)),
108
124
  });
109
125
  };
126
+ const confirmSetup = async (actions) => {
127
+ process.stdout.write("\nREA setup plan\n");
128
+ for (const action of actions)
129
+ process.stdout.write(` - ${action.detail}\n ${action.target}${action.external ? " (external software)" : ""}\n`);
130
+ const prompt = createInterface({
131
+ input: process.stdin,
132
+ output: process.stdout,
133
+ });
134
+ try {
135
+ const answer = (await prompt.question("Continue? [Y/n] "))
136
+ .trim()
137
+ .toLowerCase();
138
+ return answer === "" || answer === "y" || answer === "yes";
139
+ }
140
+ finally {
141
+ prompt.close();
142
+ }
143
+ };
110
144
  const registerXrefsCommand = (cli, logger) => {
111
145
  cli.command("xrefs", {
112
146
  description: "List bounded references to an analyzed address",
@@ -146,10 +146,10 @@ export const ENHANCED_TOOL_CONTRACTS = [
146
146
  enhanced("swift_classes", "Discover legacy-mangled Swift class procedures after exhaustively paging analyzed procedures. Returns at most 100 entries and scans at most 5,000 symbols; use analyze_swift_types for other Swift kinds.", enhancedInputSchemas.swift_classes),
147
147
  enhanced("get_objc_classes", "Discover and deduplicate Objective-C class labels after exhaustively paging names, optionally filtering by literal substring. Returns at most 100 classes; inspect matching metadata and references next.", enhancedInputSchemas.get_objc_classes),
148
148
  enhanced("get_objc_protocols", "Discover and deduplicate Objective-C and Swift protocol labels after exhaustively paging names. Returns at most 100 entries; use xrefs or analyze_function to connect a protocol to implementations.", enhancedInputSchemas.get_objc_protocols),
149
- enhanced("batch_decompile", "Decompile up to 20 explicit procedure symbols or addresses concurrently. Per-item strings may contain errors or no-output markers, so validate each result; use analyze_function for a richer single-function dossier.", enhancedInputSchemas.batch_decompile),
150
- enhanced("get_call_graph", "Traverse Hopper's caller or callee relationships from one symbol or address for at most five levels. Nodes preserve per-procedure errors; indirect calls may be missing and results are not a whole-program CFG.", enhancedInputSchemas.get_call_graph),
149
+ enhanced("batch_decompile", "Decompile up to 20 explicit procedure symbols or addresses concurrently. Returns ordered per-item ok/error variants and aggregate counts; use analyze_function for a richer single-function dossier.", enhancedInputSchemas.batch_decompile),
150
+ enhanced("get_call_graph", "Traverse Hopper's caller or callee relationships from one symbol or address for at most five levels. Every node has an ok/error status and failures use safe typed projections; indirect calls may be missing and results are not a whole-program CFG.", enhancedInputSchemas.get_call_graph),
151
151
  enhanced("analyze_swift_types", "Categorize exhaustively paged procedure names into Swift classes, structs, enums, protocols, extensions, and other symbols. Scans at most 5,000 names and returns at most 50 entries per category.", enhancedInputSchemas.analyze_swift_types),
152
- enhanced("find_xrefs_to_name", "Resolve a name through Hopper and return analyzed references to its address. Use when starting from a selector or symbol; resolution failure is returned explicitly and xrefs remain untyped.", enhancedInputSchemas.find_xrefs_to_name),
152
+ enhanced("find_xrefs_to_name", "Resolve an exact name through Hopper's exhaustively paged name inventory and return a resolved or unresolved result. Unresolved names use the stable name_not_found reason; xrefs remain untyped.", enhancedInputSchemas.find_xrefs_to_name),
153
153
  enhanced("binary_overview", "Use immediately after opening a target to summarize document, exhaustive procedure/string counts, and a bounded segment sample. detail controls segment fields and limit controls only the returned segment sample.", enhancedInputSchemas.binary_overview),
154
154
  enhanced("analyze_function", "Preferred bounded analysis for one procedure symbol or address. Returns identity, pseudocode, optional assembly, comments, calls, incoming references, and blocks; unsupported outgoing references and CFG edges carry explicit unavailable metadata.", enhancedInputSchemas.analyze_function),
155
155
  enhanced("trace_feature", "Trace a bounded literal feature query through matching strings and procedures, xrefs, and truthful containing-procedure resolution. Returns the operation budget, truncation, and residual unknowns; unknown_registry_approved: true records them durably without inferring reference kinds.", enhancedInputSchemas.trace_feature),
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { ANALYSIS_ERROR_TAGS } from "../domain/errors.js";
2
3
  import { evidenceSchema } from "../domain/evidence.js";
3
4
  import { processCaptureComparisonSchema, processCaptureSchema, } from "../domain/processCapture.js";
4
5
  import { evidenceBundleSchema } from "../domain/evidenceBundle.js";
@@ -149,9 +150,24 @@ const symbolDiscoveryOutput = (property) => resultOf(z.object({
149
150
  count: z.number().int().min(0),
150
151
  [property]: z.array(addressedEntry),
151
152
  }));
152
- const graphNode = z.union([
153
- z.object({ address: z.string(), calls: z.array(z.string()) }),
154
- z.object({ address: z.string(), error: z.string() }),
153
+ const analysisErrorProjectionSchema = z
154
+ .object({
155
+ tag: z.enum(ANALYSIS_ERROR_TAGS),
156
+ message: z.string(),
157
+ details: z.record(z.string(), z.union([z.string(), z.number(), z.null()])),
158
+ })
159
+ .strict();
160
+ const graphNode = z.discriminatedUnion("status", [
161
+ z.object({
162
+ address: z.string(),
163
+ status: z.literal("ok"),
164
+ calls: z.array(z.string()),
165
+ }),
166
+ z.object({
167
+ address: z.string(),
168
+ status: z.literal("error"),
169
+ error: analysisErrorProjectionSchema,
170
+ }),
155
171
  ]);
156
172
  const functionDossierOutput = resultOf(functionDossierSchema);
157
173
  /** Exact structured-content schemas for the direct Hopper operations. */
@@ -207,10 +223,23 @@ export const enhancedOutputSchemas = {
207
223
  swift_classes: symbolDiscoveryOutput("classes"),
208
224
  get_objc_classes: symbolDiscoveryOutput("classes"),
209
225
  get_objc_protocols: symbolDiscoveryOutput("protocols"),
210
- batch_decompile: resultOf(z.union([
211
- z.object({ error: z.literal("No addresses provided") }),
212
- z.record(z.string(), z.string()),
213
- ])),
226
+ batch_decompile: resultOf(z.object({
227
+ items: z.array(z.discriminatedUnion("status", [
228
+ z.object({
229
+ address: z.string(),
230
+ status: z.literal("ok"),
231
+ pseudocode: z.string().min(1),
232
+ }),
233
+ z.object({
234
+ address: z.string(),
235
+ status: z.literal("error"),
236
+ error: analysisErrorProjectionSchema,
237
+ }),
238
+ ])),
239
+ total: z.number().int().min(0),
240
+ succeeded: z.number().int().min(0),
241
+ failed: z.number().int().min(0),
242
+ })),
214
243
  get_call_graph: resultOf(z.record(z.string(), z.array(graphNode))),
215
244
  analyze_swift_types: resultOf(z.object({
216
245
  total: z.number().int().min(0),
@@ -219,9 +248,18 @@ export const enhancedOutputSchemas = {
219
248
  items: z.array(addressedEntry),
220
249
  })),
221
250
  })),
222
- find_xrefs_to_name: resultOf(z.union([
223
- z.object({ xrefs: addressList }),
224
- z.object({ error: z.string() }),
251
+ find_xrefs_to_name: resultOf(z.discriminatedUnion("status", [
252
+ z.object({
253
+ status: z.literal("resolved"),
254
+ name: z.string(),
255
+ address: z.string(),
256
+ xrefs: addressList,
257
+ }),
258
+ z.object({
259
+ status: z.literal("unresolved"),
260
+ name: z.string(),
261
+ reason: z.literal("name_not_found"),
262
+ }),
225
263
  ])),
226
264
  binary_overview: resultOf(z.object({
227
265
  document: z.string(),
@@ -1,3 +1,29 @@
1
+ /** Stable tags exposed by safe analysis-error projections. */
2
+ export const ANALYSIS_ERROR_TAGS = [
3
+ "AnalysisProtocolError",
4
+ "AnalysisInputError",
5
+ "AnalysisOutputError",
6
+ "AnalysisCapabilityUnavailableError",
7
+ "AnalysisCancelledError",
8
+ "AnalysisTimeoutError",
9
+ "ProviderSelectionError",
10
+ "ProviderAdapterError",
11
+ "ArtifactOperationError",
12
+ "ProcessCaptureError",
13
+ "EvidenceIntegrityError",
14
+ "EvidenceLimitError",
15
+ "EvidenceFileError",
16
+ "UnknownRegistryError",
17
+ "HopperTimeoutError",
18
+ "HopperCancelledError",
19
+ "HopperProtocolError",
20
+ "HopperRemoteError",
21
+ "HopperProcessError",
22
+ "HopperStartError",
23
+ "ConfigurationError",
24
+ "NoBinaryOpenError",
25
+ "BinaryTargetError",
26
+ ];
1
27
  /** Base class for expected analysis, provider, and session failures. */
2
28
  export class AnalysisError extends Error {
3
29
  }
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { AnalysisOutputError, HopperProtocolError } from "./errors.js";
3
3
  import { err, ok } from "./result.js";
4
+ /** Return the non-negative byte distance between hexadecimal addresses. */
5
+ export const addressDistance = (start, end) => Math.max(0, Number.parseInt(end, 16) - Number.parseInt(start, 16));
4
6
  const procedureMapSchema = z.record(z.string(), z.string());
5
7
  const addressedNamesSchema = z.array(z.object({ address: z.string(), name: z.string() }));
6
8
  const addressedNameMapSchema = z.record(z.string(), z.string());
@@ -0,0 +1,8 @@
1
+ /** Return whether a Node.js version satisfies REA's tested runtime families. */
2
+ export const supportsNodeVersion = (version) => {
3
+ const [majorText = "", minorText = ""] = version.split(".");
4
+ const major = Number.parseInt(majorText, 10);
5
+ const minor = Number.parseInt(minorText, 10);
6
+ return ((major === 22 && minor >= 19) ||
7
+ (major >= 24 && (major !== 24 || minor >= 11)));
8
+ };
@@ -81,6 +81,8 @@ class NativeMacOSClient {
81
81
  this.runner = runner;
82
82
  }
83
83
  async execute(operation, parameters, options) {
84
+ if (options?.signal?.aborted === true)
85
+ return err(new AnalysisCancelledError(operation));
84
86
  if (operation === "health")
85
87
  return ok(createAnalysisExecution(null, IDENTITY));
86
88
  if (!isNativeOperation(operation))