rea-agents 0.5.0 → 1.0.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.
package/README.md CHANGED
@@ -120,6 +120,17 @@ rea doctor
120
120
  rea analyze /Applications/Notes.app
121
121
  ```
122
122
 
123
+ Update that global installation in place:
124
+
125
+ ```bash
126
+ rea upgrade
127
+ ```
128
+
129
+ REA checks npm for the latest release and verifies that the running package is
130
+ the global installation it will replace. Source, local, and `npx` copies report
131
+ the manual `npm install --global rea-agents@latest` command instead of updating
132
+ an unrelated global package.
133
+
123
134
  Choose either the no-install commands or the global installation. You do not need both.
124
135
 
125
136
  ### Requirements
@@ -343,6 +354,7 @@ Or install the `rea` command globally:
343
354
  ```bash
344
355
  npm install --global rea-agents
345
356
  rea --help
357
+ rea upgrade
346
358
  rea mcp
347
359
  ```
348
360
 
@@ -1,6 +1,6 @@
1
1
  import { enhancedInputSchemas } from "../contracts/enhancedInputs.js";
2
- import { AnalysisInputError, AnalysisCancelledError, AnalysisOutputError, } from "../domain/errors.js";
3
- import { parseDocuments, parseFunctionDossier, parseAddressedPage, parseListCount, parseRelatedAddresses, parseSegments, } from "../domain/hopperValues.js";
2
+ import { AnalysisInputError, AnalysisCancelledError, AnalysisOutputError, projectAnalysisError, } from "../domain/errors.js";
3
+ import { addressDistance, parseDocuments, parseFunctionDossier, parseAddressedPage, parseListCount, parseRelatedAddresses, parseSegments, } from "../domain/hopperValues.js";
4
4
  import { err, ok } from "../domain/result.js";
5
5
  import { categorizeSwiftTypes, discoverObjcClasses, discoverObjcProtocols, discoverSwiftClasses, } from "../domain/symbolAnalysis.js";
6
6
  /**
@@ -89,20 +89,29 @@ export class EnhancedTools {
89
89
  return names.ok ? ok(discoverObjcProtocols(names.value)) : names;
90
90
  }
91
91
  async #batchDecompile(addresses, signal) {
92
- if (addresses.length === 0)
93
- return ok({ error: "No addresses provided" });
94
- const entries = await Promise.all(addresses.map(async (address) => {
92
+ const items = await Promise.all(addresses.map(async (address) => {
95
93
  const result = await this.#call("procedure_pseudo_code", { procedure: address }, signal);
96
- return [
97
- address,
98
- result.ok
99
- ? result.value === null || result.value === ""
100
- ? "No output"
101
- : result.value
102
- : `Error: ${result.error.message}`,
103
- ];
94
+ if (!result.ok)
95
+ return {
96
+ address,
97
+ status: "error",
98
+ error: projectAnalysisError(result.error),
99
+ };
100
+ if (typeof result.value !== "string" || result.value.length === 0)
101
+ return {
102
+ address,
103
+ status: "error",
104
+ error: projectAnalysisError(new AnalysisOutputError("procedure_pseudo_code", "provider returned empty pseudocode")),
105
+ };
106
+ return { address, status: "ok", pseudocode: result.value };
104
107
  }));
105
- return ok(Object.fromEntries(entries));
108
+ const succeeded = items.filter(({ status }) => status === "ok").length;
109
+ return ok({
110
+ items,
111
+ total: items.length,
112
+ succeeded,
113
+ failed: items.length - succeeded,
114
+ });
106
115
  }
107
116
  async #callGraph(input, signal) {
108
117
  const relation = input.direction === "forward" ? "callees" : "callers";
@@ -124,7 +133,8 @@ export class EnhancedTools {
124
133
  if (!result.ok) {
125
134
  graph[level].push({
126
135
  address: current.address,
127
- error: result.error.message,
136
+ status: "error",
137
+ error: projectAnalysisError(result.error),
128
138
  });
129
139
  continue;
130
140
  }
@@ -132,12 +142,14 @@ export class EnhancedTools {
132
142
  if (!related.ok) {
133
143
  graph[level].push({
134
144
  address: current.address,
135
- error: related.error.message,
145
+ status: "error",
146
+ error: projectAnalysisError(related.error),
136
147
  });
137
148
  continue;
138
149
  }
139
150
  graph[level].push({
140
151
  address: current.address,
152
+ status: "ok",
141
153
  calls: [...related.value],
142
154
  });
143
155
  if (current.depth + 1 < input.depth) {
@@ -158,16 +170,24 @@ export class EnhancedTools {
158
170
  : procedures;
159
171
  }
160
172
  async #findXrefs(name, signal) {
161
- const resolved = await this.#call("address_name", { address: name }, signal);
162
- if (!resolved.ok)
163
- return resolved;
164
- const address = resolveAddress(resolved.value);
165
- if (address === undefined)
166
- return ok({ error: `Could not resolve name: ${name}` });
167
- const xrefs = await this.#call("xrefs", { address }, signal);
173
+ const names = await this.#allAddressed("list_names", signal);
174
+ if (!names.ok)
175
+ return names;
176
+ const resolved = names.value.find((entry) => entry.name === name);
177
+ if (resolved === undefined)
178
+ return ok({ status: "unresolved", name, reason: "name_not_found" });
179
+ const xrefs = await this.#call("xrefs", { address: resolved.address }, signal);
168
180
  if (!xrefs.ok)
169
181
  return xrefs;
170
- return ok(Array.isArray(xrefs.value) ? { xrefs: xrefs.value } : xrefs.value);
182
+ if (!Array.isArray(xrefs.value) ||
183
+ xrefs.value.some((xref) => typeof xref !== "string"))
184
+ return err(new AnalysisOutputError("xrefs", "provider returned an invalid address list"));
185
+ return ok({
186
+ status: "resolved",
187
+ name,
188
+ address: resolved.address,
189
+ xrefs: xrefs.value,
190
+ });
171
191
  }
172
192
  async #binaryOverview(input, signal) {
173
193
  const [segmentsResult, documentsResult, procedures, stringsResult] = await Promise.all([
@@ -346,15 +366,3 @@ export class EnhancedTools {
346
366
  const invalidInput = (name, cause) => Promise.resolve(err(new AnalysisInputError(name, {
347
367
  cause,
348
368
  })));
349
- const resolveAddress = (value) => {
350
- if (typeof value === "string" && value.length > 0)
351
- return value;
352
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
353
- return undefined;
354
- }
355
- const candidate = value.address ?? value.name;
356
- return typeof candidate === "string" && candidate.length > 0
357
- ? candidate
358
- : undefined;
359
- };
360
- const addressDistance = (start, end) => Math.max(0, Number.parseInt(end, 16) - Number.parseInt(start, 16));
@@ -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
@@ -4,6 +4,7 @@ import { runDoctor } from "./application/Doctor.js";
4
4
  import { runDirectAnalysis, runProviderAnalysis, runSessionStatus, } from "./application/DirectAnalysis.js";
5
5
  import { runSetup } from "./application/Setup.js";
6
6
  import { runUninstall } from "./application/Uninstall.js";
7
+ import { runUpgrade, systemUpgradeHost } from "./application/Upgrade.js";
7
8
  import { PRODUCT_IDENTITY } from "./identity.js";
8
9
  import { createLogger, parseLogLevel } from "./logger.js";
9
10
  import { logCliCommand } from "./cliLogging.js";
@@ -82,6 +83,10 @@ const registerCoreCommands = (cli, logger) => {
82
83
  alias: { purgeData: "purge-data" },
83
84
  run: ({ options }) => logCliCommand(logger, "uninstall", () => runUninstall(options.purgeData)),
84
85
  });
86
+ cli.command("upgrade", {
87
+ description: "Upgrade a global npm installation to the latest REA release",
88
+ run: ({ formatExplicit }) => logCliCommand(logger, "upgrade", () => runUpgrade(process.env.REA_PACKAGE_VERSION ?? "0.0.0-development", systemUpgradeHost(), formatExplicit ? "structured" : "human")),
89
+ });
85
90
  cli.command("analyze", {
86
91
  description: "Get an overview of an app",
87
92
  args: z.object({
@@ -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());
@@ -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))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rea-agents",
3
- "version": "0.5.0",
3
+ "version": "1.0.0",
4
4
  "description": "Reverse engineer anything from your terminal or coding agent with one CLI and MCP server.",
5
5
  "license": "MIT",
6
6
  "repository": {