rea-agents 1.0.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.
package/README.md CHANGED
@@ -11,14 +11,14 @@
11
11
  [![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
12
12
  [![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
13
13
  [![68 MCP tools](https://img.shields.io/badge/MCP_tools-68-5c4ee5?style=flat-square)](#68-tools-for-investigation)
14
- [![Node.js 24](https://img.shields.io/badge/Node.js-24.18.x-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
14
+ [![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
15
15
  [![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
16
16
 
17
17
  [Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [68 tools](#68-tools-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
18
18
 
19
19
  <br />
20
20
 
21
- <code>curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash</code>
21
+ <code>npm install --global rea-agents && rea setup</code>
22
22
 
23
23
  </div>
24
24
 
@@ -81,15 +81,24 @@ REA shows how it reached its conclusions. It does not claim to recover original
81
81
 
82
82
  ## Quick start
83
83
 
84
- ### One-command install — recommended
84
+ ### Install the CLI — recommended
85
85
 
86
86
  ```bash
87
- curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
87
+ npm install --global rea-agents
88
+ rea setup
88
89
  ```
89
90
 
90
- The installer supports macOS 12+, Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux. It installs compatible Node.js 24/npm 11 prerequisites, the exact latest `rea-agents-*` release, the official Hopper package, detected client registrations, and the bundled skill, then verifies the result. macOS uses Homebrew; Linux uses the native package manager and may show a system authorization prompt. Pin a reproducible release with `REA_VERSION=0.3.0`; `REA_VERSION` is the only supported installer environment override.
91
+ Installing the CLI does not update Homebrew, Node.js, npm, Hopper, or coding-agent configuration. `rea setup` detects what is already present, prints every proposed change, and asks before applying it.
92
+
93
+ REA detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, and Devin. Registrations are additive, backup-first, and read back after writing. You can safely rerun setup.
91
94
 
92
- REA detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, and Devin. It configures detected clients with documented local MCP files; Devin is reported as skipped because its public documentation does not define a local configuration file. You can safely rerun the installer or `rea setup --yes`. If REA's local analysis engine needs one-time activation, installation remains intact and setup reports the exact remaining action rather than claiming full readiness.
95
+ An optional curl wrapper installs the same CLI package and starts setup only when a terminal is available:
96
+
97
+ ```bash
98
+ curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
99
+ ```
100
+
101
+ Pass installer options after `bash -s --`, for example `--dry-run`, `--no-setup`, or `--version 1.0.0`. The curl wrapper never installs prerequisites or configures integrations itself. See [Installation and setup](docs/installation.md) for its exact mutation boundary.
93
102
 
94
103
  ### With a coding agent — recommended
95
104
 
@@ -99,23 +108,23 @@ npx skills add morluto/rea
99
108
 
100
109
  Ask your agent to set up REA. It will check your Mac, explain anything it needs to install, ask for approval, and guide you through system prompts. After setup, restart the agent if it asks you to load the full REA toolset.
101
110
 
102
- Approve installation if needed, complete the one-time activation when prompted, then describe the app or feature you want to understand. REA handles the binary-analysis tools behind the scenes.
111
+ Review the setup plan, approve it if appropriate, complete Hopper's one-time activation when prompted, then describe the app or feature you want to understand.
103
112
 
104
113
  ### From Terminal — no installation
105
114
 
106
115
  ```bash
107
- npx -y rea-agents setup --yes
116
+ npx -y rea-agents setup
108
117
  npx -y rea-agents doctor
109
118
  npx -y rea-agents analyze /Applications/Notes.app
110
119
  ```
111
120
 
112
- If macOS or an installer asks for confirmation, complete the prompt and run the same command again. Restart a configured coding agent so it loads REA.
121
+ Review the setup plan before confirming it. Restart a configured coding agent so it loads REA.
113
122
 
114
123
  ### From Terminal — install the `rea` command
115
124
 
116
125
  ```bash
117
126
  npm install --global rea-agents
118
- rea setup --yes
127
+ rea setup
119
128
  rea doctor
120
129
  rea analyze /Applications/Notes.app
121
130
  ```
@@ -137,9 +146,10 @@ Choose either the no-install commands or the global installation. You do not nee
137
146
 
138
147
  - macOS 12 or newer
139
148
  - Ubuntu 24.04+, Fedora 41+, or 64-bit Arch Linux
140
- - Node.js 24.18.x with npm 11.16.x
149
+ - Node.js 22.19+ or 24.11+ (including newer releases)
150
+ - npm; REA does not require or install a particular npm version
141
151
 
142
- You do not need to choose or install binary-analysis tools yourself. REA setup handles them when needed. Deep binary analysis currently uses [Hopper](https://www.hopperapp.com/), a separate desktop application with its own license. REA can install the official macOS or Linux package, but you must approve installation and complete its one-time activation.
152
+ Deep binary analysis currently uses [Hopper](https://www.hopperapp.com/), a separate desktop application with its own license. Setup reuses an existing installation. If Hopper is missing, interactive setup proposes the official package and includes it in the confirmation plan. Unattended installation requires `rea setup --yes --install-hopper`.
143
153
 
144
154
  If something is not working, run:
145
155
 
@@ -151,7 +161,9 @@ npx -y rea-agents doctor
151
161
 
152
162
  ### Linux installation and troubleshooting
153
163
 
154
- On Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux, setup downloads the matching official Hopper package, restricts downloads to Hopper's public origin, verifies the published size and checksum, and invokes `apt-get`, `dnf`, or `pacman`. When REA is not already running as root, `pkexec` presents the system authorization prompt. REA never invokes `sudo`.
164
+ On macOS, approved setup downloads Hopper's official DMG, verifies it, and installs the app into `~/Applications` without Homebrew or administrator privileges. It opens Hopper once for activation; no manual drag-and-drop is required.
165
+
166
+ On Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux, approved setup downloads the matching official Hopper package, restricts downloads to Hopper's public origin, verifies the published size and checksum, and invokes `apt-get`, `dnf`, or `pacman`. When REA is not already running as root, `pkexec` presents the system authorization prompt. REA never invokes `sudo`.
155
167
 
156
168
  The normal Linux launcher is `/opt/hopper/bin/Hopper`. If Hopper was installed elsewhere:
157
169
 
@@ -166,7 +178,7 @@ If doctor reports a missing analysis engine even though the file exists, inspect
166
178
  ldd /opt/hopper/bin/Hopper | grep 'not found'
167
179
  ```
168
180
 
169
- Install the missing distribution packages and rerun `rea setup --yes`. Hopper is a desktop application: real analysis requires an active `DISPLAY` or `WAYLAND_DISPLAY`, plus one-time license activation. The curl installer places the `rea` command in `~/.local/bin` on Linux; add that directory to future shell `PATH` values if it is not already present.
181
+ Install the missing distribution packages and rerun `rea setup`. Hopper is a desktop application: real analysis requires an active `DISPLAY` or `WAYLAND_DISPLAY`, plus one-time license activation. The curl installer places the `rea` command in `~/.local/bin` on Linux; add that directory to future shell `PATH` values if it is not already present.
170
182
 
171
183
  REA defaults `HOPPER_LAUNCHER_PATH` to `/Applications/Hopper Disassembler.app/Contents/MacOS/hopper` on macOS and `/opt/hopper/bin/Hopper` on Linux. Explicit configuration always takes precedence.
172
184
 
@@ -177,7 +189,7 @@ rea uninstall
177
189
  rea uninstall --purge-data # also removes only ~/.rea/cache and ~/.rea/state
178
190
  ```
179
191
 
180
- Uninstall preserves Hopper, Homebrew, Node.js, evidence, captures, external evidence roots, unrelated skills, and other MCP servers. It refuses malformed client configuration and never follows purge-data symlinks.
192
+ Uninstall preserves Hopper, Node.js, evidence, captures, external evidence roots, unrelated skills, and other MCP servers. It refuses malformed client configuration and never follows purge-data symlinks.
181
193
 
182
194
  ### CLI or coding agent?
183
195
 
@@ -286,7 +298,7 @@ REA is growing into a toolkit for understanding software across static artifacts
286
298
  7. **More targets and platforms** — Windows-native providers and ConPTY verification, Linux parity, websites and APIs, mobile artifacts, firmware, document formats, and other software-defined systems.
287
299
  8. **Differential reconstruction** — compare artifacts, functions, bundles, protocols, UIs, and process captures; track residual unknowns; verify a reconstruction against observed behavior.
288
300
 
289
- Roadmap items describe direction, not shipped support. New providers must produce the same evidence and safety metadata as existing capabilities before they become part of the public workflow.
301
+ Roadmap items describe direction, not shipped support. New providers must produce the same evidence and safety metadata as existing capabilities before they become part of the public workflow. Once REA has multiple optional toolchains, setup can become capability-selective; the consent rules for that future work are recorded in the [installation roadmap](docs/roadmap.md).
290
302
 
291
303
  See the [static-analysis provider evaluation](docs/provider-evaluation.md) for the current research matrix and admission gate.
292
304
 
@@ -6,6 +6,7 @@ import { join } from "node:path";
6
6
  import { execFile } from "node:child_process";
7
7
  import { promisify } from "node:util";
8
8
  import { parseBinaryTarget } from "../domain/binaryTarget.js";
9
+ import { supportsNodeVersion } from "../domain/runtimeVersion.js";
9
10
  import { probeHomebrew } from "./homebrew.js";
10
11
  import { linuxHopperLauncherPath, linuxSharedLibrariesAvailable, readLinuxDistribution, } from "./LinuxHopper.js";
11
12
  const execFileAsync = promisify(execFile);
@@ -18,9 +19,8 @@ const SYSTEM_LINUX_HOPPER = "/opt/hopper/bin/Hopper";
18
19
  */
19
20
  export const runDoctor = async (target, host = systemDoctorHost()) => {
20
21
  const checks = [];
21
- const nodeMajor = parseMajor(host.nodeVersion);
22
- checks.push(check("node", nodeMajor >= 24, host.nodeVersion, {
23
- remediation: "Install Node.js 24.18 or newer.",
22
+ checks.push(check("node", supportsNodeVersion(host.nodeVersion), host.nodeVersion, {
23
+ remediation: "Install Node.js 22.19+ or 24.11+.",
24
24
  classification: "missing_dependency",
25
25
  }));
26
26
  const macosVersion = host.platform === "darwin" ? await host.macosVersion() : undefined;
@@ -50,7 +50,7 @@ export const runDoctor = async (target, host = systemDoctorHost()) => {
50
50
  break;
51
51
  }
52
52
  checks.push(check("hopper", hopperPath !== undefined, hopperPath, {
53
- remediation: "Run rea setup --yes to install Hopper, or set HOPPER_LAUNCHER_PATH.",
53
+ remediation: "Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.",
54
54
  classification: "missing_analysis_engine",
55
55
  }));
56
56
  if (target !== undefined)
@@ -60,8 +60,9 @@ export const writeEvidenceBundle = async (bundle, path, overwrite, policy) => {
60
60
  if (bytes > policy.maxBytes)
61
61
  return err(new EvidenceFileError("write", "too-large"));
62
62
  try {
63
- const canonicalParent = await realpath(dirname(resolve(path)));
64
- const destination = resolve(canonicalParent, basename(resolve(path)));
63
+ const requestedPath = resolve(path);
64
+ const canonicalParent = await realpath(dirname(requestedPath));
65
+ const destination = resolve(canonicalParent, basename(requestedPath));
65
66
  if (!(await isApproved(destination, policy.roots)))
66
67
  return err(new EvidenceFileError("write", "outside-root"));
67
68
  const existing = await lstat(destination).catch((cause) => {
@@ -80,7 +81,7 @@ export const writeEvidenceBundle = async (bundle, path, overwrite, policy) => {
80
81
  mode: 0o600,
81
82
  fsync: true,
82
83
  });
83
- return ok({ path: destination, bytes });
84
+ return ok({ path: requestedPath, bytes });
84
85
  }
85
86
  catch (cause) {
86
87
  return err(new EvidenceFileError("write", "io", { cause }));
@@ -0,0 +1,175 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFile } from "node:child_process";
3
+ import { access, mkdir, mkdtemp, readdir, rename, rm, writeFile, } from "node:fs/promises";
4
+ import { homedir, tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { z } from "zod";
8
+ const execFileAsync = promisify(execFile);
9
+ const RELEASES_URL = "https://www.hopperapp.com/include/files-api.php?request=releases&public=true";
10
+ const DOWNLOAD_PREFIX = "https://www.hopperapp.com:443/downloader/public/";
11
+ const MAX_PACKAGE_BYTES = 100_000_000;
12
+ const BUNDLE_IDENTIFIER = "com.cryptic-apps.hopper-web-4";
13
+ /** Download, verify, and install Hopper into the current user's Applications directory. */
14
+ export const installMacHopper = async (host = systemMacHopperInstallHost()) => {
15
+ let temporary;
16
+ let mounted = false;
17
+ try {
18
+ const metadata = await host.download(RELEASES_URL);
19
+ if (!metadata.ok)
20
+ return { status: "failed", reason: "release_metadata" };
21
+ const parsed = releasesSchema.safeParse(parseJson(metadata.bytes));
22
+ if (!parsed.success)
23
+ return { status: "failed", reason: "release_metadata" };
24
+ const release = parsed.data["OS X"];
25
+ if (!release.filename.startsWith(DOWNLOAD_PREFIX))
26
+ return { status: "failed", reason: "release_metadata" };
27
+ const archive = await host.download(release.filename);
28
+ if (!archive.ok)
29
+ return { status: "failed", reason: "download" };
30
+ if (!integrityMatches(archive.bytes, release))
31
+ return { status: "failed", reason: "integrity" };
32
+ const destination = host.destination();
33
+ if (await host.destinationExists(destination))
34
+ return { status: "failed", reason: "destination_exists" };
35
+ temporary = await host.createTemporaryDirectory();
36
+ const packagePath = join(temporary, "hopper.dmg");
37
+ const mountPath = join(temporary, "mounted");
38
+ await host.createMountDirectory(mountPath);
39
+ await host.writePackage(packagePath, archive.bytes);
40
+ if (!(await host.mount(packagePath, mountPath)))
41
+ return { status: "failed", reason: "mount" };
42
+ mounted = true;
43
+ const source = await host.appBundle(mountPath);
44
+ if (source === undefined || !(await host.validBundle(source)))
45
+ return { status: "failed", reason: "bundle" };
46
+ const stage = `${destination}.rea-stage`;
47
+ if (!(await host.installBundle(source, destination, stage)))
48
+ return { status: "failed", reason: "copy" };
49
+ const launcherPath = join(destination, "Contents/MacOS/hopper");
50
+ if (!(await host.launcherReady(launcherPath)))
51
+ return { status: "failed", reason: "launcher_missing" };
52
+ await host.openApplication(destination);
53
+ return { status: "installed", launcherPath };
54
+ }
55
+ catch {
56
+ return { status: "failed", reason: "download" };
57
+ }
58
+ finally {
59
+ if (temporary !== undefined) {
60
+ const mountPath = join(temporary, "mounted");
61
+ if (mounted)
62
+ await host.unmount(mountPath);
63
+ await host.cleanup(temporary);
64
+ }
65
+ }
66
+ };
67
+ const systemMacHopperInstallHost = () => ({
68
+ async download(url) {
69
+ const response = await fetch(url, {
70
+ headers: { accept: "application/json", "user-agent": "rea-installer" },
71
+ signal: AbortSignal.timeout(120_000),
72
+ });
73
+ const length = Number(response.headers.get("content-length") ?? "0");
74
+ if (length > MAX_PACKAGE_BYTES)
75
+ return { ok: false, bytes: new Uint8Array() };
76
+ const bytes = new Uint8Array(await response.arrayBuffer());
77
+ return { ok: response.ok && bytes.byteLength <= MAX_PACKAGE_BYTES, bytes };
78
+ },
79
+ createTemporaryDirectory: () => mkdtemp(join(tmpdir(), "rea-hopper-mac-")),
80
+ createMountDirectory: (path) => mkdir(path),
81
+ writePackage: (path, bytes) => writeFile(path, bytes, { mode: 0o600 }),
82
+ async mount(packagePath, mountPath) {
83
+ return commandSucceeds("hdiutil", [
84
+ "attach",
85
+ "-readonly",
86
+ "-nobrowse",
87
+ "-mountpoint",
88
+ mountPath,
89
+ packagePath,
90
+ ]);
91
+ },
92
+ async unmount(mountPath) {
93
+ await commandSucceeds("hdiutil", ["detach", mountPath, "-force"]);
94
+ },
95
+ async appBundle(mountPath) {
96
+ const entries = await readdir(mountPath, { withFileTypes: true });
97
+ const app = entries.find((entry) => entry.isDirectory() && /^Hopper.*\.app$/iu.test(entry.name));
98
+ return app === undefined ? undefined : join(mountPath, app.name);
99
+ },
100
+ async validBundle(appPath) {
101
+ if (!(await commandSucceeds("codesign", [
102
+ "--verify",
103
+ "--deep",
104
+ "--strict",
105
+ appPath,
106
+ ])))
107
+ return false;
108
+ try {
109
+ const identifier = (await execFileAsync("/usr/libexec/PlistBuddy", [
110
+ "-c",
111
+ "Print :CFBundleIdentifier",
112
+ join(appPath, "Contents/Info.plist"),
113
+ ])).stdout.trim();
114
+ return identifier === BUNDLE_IDENTIFIER;
115
+ }
116
+ catch {
117
+ return false;
118
+ }
119
+ },
120
+ async destinationExists(path) {
121
+ try {
122
+ await access(path);
123
+ return true;
124
+ }
125
+ catch {
126
+ return false;
127
+ }
128
+ },
129
+ async installBundle(source, destination, stage) {
130
+ try {
131
+ await mkdir(join(homedir(), "Applications"), { recursive: true });
132
+ await rm(stage, { recursive: true, force: true });
133
+ await execFileAsync("ditto", [source, stage]);
134
+ await rename(stage, destination);
135
+ return true;
136
+ }
137
+ catch {
138
+ await rm(stage, { recursive: true, force: true });
139
+ return false;
140
+ }
141
+ },
142
+ async launcherReady(path) {
143
+ try {
144
+ await access(path);
145
+ return true;
146
+ }
147
+ catch {
148
+ return false;
149
+ }
150
+ },
151
+ async openApplication(path) {
152
+ await execFileAsync("open", [path]);
153
+ },
154
+ cleanup: (path) => rm(path, { recursive: true, force: true }),
155
+ destination: () => join(homedir(), "Applications/Hopper Disassembler.app"),
156
+ });
157
+ const parseJson = (bytes) => JSON.parse(new TextDecoder().decode(bytes));
158
+ const integrityMatches = (bytes, release) => bytes.byteLength === Number(release.file_length) &&
159
+ bytes.byteLength <= MAX_PACKAGE_BYTES &&
160
+ createHash("sha1").update(bytes).digest("hex") === release.file_hash;
161
+ const commandSucceeds = async (command, args) => {
162
+ try {
163
+ await execFileAsync(command, args);
164
+ return true;
165
+ }
166
+ catch {
167
+ return false;
168
+ }
169
+ };
170
+ const releaseSchema = z.object({
171
+ filename: z.string().url(),
172
+ file_length: z.string().regex(/^\d+$/u),
173
+ file_hash: z.string().regex(/^[a-f0-9]{40}$/u),
174
+ });
175
+ const releasesSchema = z.object({ "OS X": releaseSchema });
@@ -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 {
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
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";
@@ -60,10 +61,20 @@ const registerCoreCommands = (cli, logger) => {
60
61
  yes: z
61
62
  .boolean()
62
63
  .default(false)
63
- .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"),
64
69
  }),
65
- alias: { yes: "y" },
66
- 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)),
67
78
  });
68
79
  cli.command("doctor", {
69
80
  description: "Check whether REA is ready",
@@ -112,6 +123,24 @@ const registerCoreCommands = (cli, logger) => {
112
123
  run: ({ args }) => logCliCommand(logger, "decompile", () => runDirectAnalysis(args.path, "procedure_pseudo_code", { procedure: args.address }, logger)),
113
124
  });
114
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
+ };
115
144
  const registerXrefsCommand = (cli, logger) => {
116
145
  cli.command("xrefs", {
117
146
  description: "List bounded references to an analyzed address",
@@ -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
+ };
package/install.sh CHANGED
@@ -3,196 +3,111 @@ set -euo pipefail
3
3
 
4
4
  PACKAGE="rea-agents"
5
5
  REPOSITORY="morluto/rea"
6
- NODE_FORMULA="node@24"
7
- NODE_VERSION="24.18.0"
8
- temporary_directory=""
9
- node_destination=""
10
- node_stage=""
11
- node_backup=""
12
- node_version=""
13
-
14
- cleanup() {
15
- if [[ -n "$temporary_directory" && -d "$temporary_directory" ]]; then
16
- rm -rf -- "$temporary_directory"
17
- fi
18
- if [[ -n "$node_stage" && -e "$node_stage" ]]; then
19
- rm -rf -- "$node_stage"
20
- fi
21
- if [[ -n "$node_backup" && -e "$node_backup" && ! -e "$node_destination" ]]; then
22
- mv -- "$node_backup" "$node_destination"
23
- fi
24
- }
25
- trap cleanup EXIT HUP INT TERM
6
+ version="${REA_VERSION:-}"
7
+ dry_run=false
8
+ start_setup=true
9
+ verbose=false
26
10
 
27
11
  fail() {
28
12
  printf 'REA installation failed: %s\n' "$1" >&2
29
13
  exit 1
30
14
  }
31
15
 
32
- version_at_least() {
33
- local actual="$1" required_major="$2" required_minor="$3"
34
- local major minor
35
- major="${actual%%.*}"
36
- minor="${actual#*.}"
37
- minor="${minor%%.*}"
38
- [[ "$major" =~ ^[0-9]+$ && "$minor" =~ ^[0-9]+$ ]] || return 1
39
- ((major > required_major || (major == required_major && minor >= required_minor)))
16
+ usage() {
17
+ cat <<'EOF'
18
+ Usage: install.sh [--version VERSION] [--dry-run] [--no-setup] [--no-prompt] [--verbose]
19
+
20
+ Installs only the REA CLI. Run `rea setup` separately to configure Hopper and coding agents.
21
+ EOF
40
22
  }
41
23
 
42
- platform="$(uname -s)"
43
- if [[ "$platform" == "Darwin" ]]; then
44
- macos_version="$(sw_vers -productVersion 2>/dev/null)" || fail "could not determine the macOS version."
45
- macos_major="${macos_version%%.*}"
46
- [[ "$macos_major" =~ ^[0-9]+$ && "$macos_major" -ge 12 ]] || fail "macOS 12 or newer is required."
47
- elif [[ "$platform" == "Linux" ]]; then
48
- [[ "$(uname -m)" == "x86_64" || "$(uname -m)" == "aarch64" ]] || fail "64-bit x86 or ARM Linux is required."
49
- [[ -r /etc/os-release ]] || fail "could not identify the Linux distribution."
50
- linux_id="$(sed -n 's/^ID=//p' /etc/os-release | tr -d '"')"
51
- linux_version="$(sed -n 's/^VERSION_ID=//p' /etc/os-release | tr -d '"')"
52
- case "$linux_id" in
53
- ubuntu) [[ "${linux_version%%.*}" -ge 24 ]] || fail "Ubuntu 24.04 or newer is required." ;;
54
- fedora) [[ "${linux_version%%.*}" -ge 41 ]] || fail "Fedora 41 or newer is required." ;;
55
- arch) ;;
56
- *) fail "supported Linux distributions are Ubuntu 24.04+, Fedora 41+, and Arch Linux." ;;
24
+ while (($# > 0)); do
25
+ case "$1" in
26
+ --version)
27
+ (($# >= 2)) || fail "--version requires a value."
28
+ version="$2"
29
+ shift 2
30
+ ;;
31
+ --dry-run)
32
+ dry_run=true
33
+ shift
34
+ ;;
35
+ --no-setup|--no-prompt)
36
+ start_setup=false
37
+ shift
38
+ ;;
39
+ --verbose)
40
+ verbose=true
41
+ shift
42
+ ;;
43
+ --help|-h)
44
+ usage
45
+ exit 0
46
+ ;;
47
+ *) fail "unknown option: $1" ;;
57
48
  esac
58
- else
59
- fail "REA supports macOS and selected 64-bit Linux distributions."
60
- fi
61
- command -v curl >/dev/null 2>&1 || fail "curl is required."
62
- command -v shasum >/dev/null 2>&1 || command -v sha256sum >/dev/null 2>&1 || fail "shasum or sha256sum is required."
49
+ done
63
50
 
64
- if [[ "$platform" == "Darwin" ]] && ! command -v brew >/dev/null 2>&1; then
65
- printf 'Installing Homebrew prerequisite...\n'
66
- NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || fail "Homebrew installation did not complete."
67
- if [[ -x /opt/homebrew/bin/brew ]]; then
68
- PATH="/opt/homebrew/bin:$PATH"
69
- elif [[ -x /usr/local/bin/brew ]]; then
70
- PATH="/usr/local/bin:$PATH"
71
- fi
72
- fi
73
- if [[ "$platform" == "Darwin" ]]; then
74
- command -v brew >/dev/null 2>&1 || fail "Homebrew is not available after installation."
51
+ if [[ "$verbose" == true ]]; then
52
+ set -x
75
53
  fi
76
54
 
77
- node_compatible=false
78
- if command -v node >/dev/null 2>&1; then
79
- node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
80
- fi
81
- if version_at_least "$node_version" 24 18 && [[ "$node_version" =~ ^24\. ]]; then
82
- node_compatible=true
83
- fi
84
- if [[ "$node_compatible" != true ]]; then
85
- if [[ "$platform" == "Darwin" ]]; then
86
- brew install "$NODE_FORMULA" || fail "Node.js 24 installation failed."
87
- node_prefix="$(brew --prefix "$NODE_FORMULA")" || fail "could not locate Node.js 24."
88
- PATH="$node_prefix/bin:$PATH"
89
- else
90
- machine="$(uname -m)"
91
- node_arch="$([[ "$machine" == "x86_64" ]] && printf 'x64' || printf 'arm64')"
92
- node_archive="node-v${NODE_VERSION}-linux-${node_arch}.tar.xz"
93
- node_base="https://nodejs.org/dist/v${NODE_VERSION}"
94
- temporary_directory="$(mktemp -d "${TMPDIR:-/tmp}/rea-install.XXXXXX")" || fail "could not create a temporary directory."
95
- curl -fsSL "$node_base/$node_archive" -o "$temporary_directory/$node_archive" || fail "Node.js download failed."
96
- curl -fsSL "$node_base/SHASUMS256.txt" -o "$temporary_directory/SHASUMS256.txt" || fail "Node.js checksum download failed."
97
- expected_checksum="$(awk -v archive="$node_archive" '$2 == archive { print $1 }' "$temporary_directory/SHASUMS256.txt")"
98
- [[ "$expected_checksum" =~ ^[a-f0-9]{64}$ ]] || fail "Node.js checksum metadata was malformed."
99
- if command -v shasum >/dev/null 2>&1; then
100
- actual_checksum="$(shasum -a 256 "$temporary_directory/$node_archive" | awk '{ print $1 }')"
101
- else
102
- actual_checksum="$(sha256sum "$temporary_directory/$node_archive" | awk '{ print $1 }')"
103
- fi
104
- [[ "$actual_checksum" == "$expected_checksum" ]] || fail "Node.js checksum verification failed."
105
- node_destination="$HOME/.local/share/rea/node"
106
- node_stage="$HOME/.local/share/rea/node.new.$$"
107
- node_backup="$HOME/.local/share/rea/node.backup.$$"
108
- [[ ! -L "$node_destination" ]] || fail "refusing to replace a symlinked Node.js installation."
109
- mkdir -p "$HOME/.local/share/rea"
110
- rm -rf -- "$node_stage" "$node_backup"
111
- mkdir "$node_stage"
112
- tar -xJf "$temporary_directory/$node_archive" --strip-components=1 -C "$node_stage" || fail "Node.js extraction failed."
113
- if [[ -e "$node_destination" ]]; then
114
- mv -- "$node_destination" "$node_backup"
115
- fi
116
- if ! mv -- "$node_stage" "$node_destination"; then
117
- [[ ! -e "$node_backup" ]] || mv -- "$node_backup" "$node_destination"
118
- fail "Node.js installation replacement failed."
119
- fi
120
- node_stage=""
121
- rm -rf -- "$node_backup"
122
- node_backup=""
123
- PATH="$node_destination/bin:$PATH"
124
- fi
125
- fi
55
+ platform="$(uname -s)"
56
+ [[ "$platform" == "Darwin" || "$platform" == "Linux" ]] || fail "REA supports macOS and selected 64-bit Linux distributions."
57
+ command -v curl >/dev/null 2>&1 || fail "curl is required."
58
+ command -v node >/dev/null 2>&1 || fail "Node.js 22.19+ or 24.11+ is required; install it from https://nodejs.org and retry."
59
+ command -v npm >/dev/null 2>&1 || fail "npm is required; install it with Node.js and retry."
60
+
126
61
  node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
127
- if ! command -v node >/dev/null 2>&1 || ! version_at_least "$node_version" 24 18 || [[ ! "$node_version" =~ ^24\. ]]; then
128
- fail "Node.js 24.18 or newer is not active on PATH."
129
- fi
130
- command -v npm >/dev/null 2>&1 || fail "npm is missing from the Node.js installation."
131
- npm_version="$(npm --version)"
132
- if [[ ! "$npm_version" =~ ^11\. ]] || ! version_at_least "$npm_version" 11 16; then
133
- if [[ "$platform" == "Linux" ]]; then
134
- npm install --global --prefix "$HOME/.local" npm@11 || fail "npm 11 installation failed."
135
- else
136
- npm install --global npm@11 || fail "npm 11 installation failed."
137
- fi
138
- fi
139
- npm_version="$(npm --version)"
140
- if [[ ! "$npm_version" =~ ^11\. ]] || ! version_at_least "$npm_version" 11 16; then
141
- fail "npm 11.16 or newer is not active on PATH."
62
+ IFS=. read -r node_major node_minor _node_patch <<<"$node_version"
63
+ [[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "could not determine the active Node.js version."
64
+ if ! ((node_major == 22 && node_minor >= 19 || node_major >= 24)); then
65
+ fail "Node.js $node_version is unsupported; use Node.js 22.19+ or 24.11+."
142
66
  fi
143
- if [[ -n "$temporary_directory" ]]; then
144
- rm -rf -- "$temporary_directory"
145
- temporary_directory=""
67
+ if ((node_major == 24 && node_minor < 11)); then
68
+ fail "Node.js $node_version is unsupported; use Node.js 24.11 or newer."
146
69
  fi
147
70
 
148
- if [[ -n "${REA_VERSION:-}" ]]; then
149
- version="$REA_VERSION"
150
- [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || fail "REA_VERSION must be an exact semantic version."
71
+ if [[ -n "$version" ]]; then
72
+ [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || fail "version must be an exact semantic version."
151
73
  else
152
- release_json="$(curl -fsSL -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$REPOSITORY/releases/latest")" || fail "could not resolve the latest GitHub release."
74
+ release_json="$(curl -fsSL --proto '=https' --tlsv1.2 --retry 3 -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$REPOSITORY/releases/latest")" || fail "could not resolve the latest GitHub release."
153
75
  tag="$(printf '%s' "$release_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{const v=JSON.parse(s).tag_name;if(typeof v!=="string")process.exit(1);process.stdout.write(v)})')" || fail "GitHub returned malformed release metadata."
154
76
  [[ "$tag" =~ ^rea-agents-([0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?)$ ]] || fail "latest release tag is not a valid rea-agents release."
155
77
  version="${BASH_REMATCH[1]}"
156
78
  fi
157
79
 
158
- printf 'Installing %s@%s...\n' "$PACKAGE" "$version"
80
+ prefix_args=()
159
81
  if [[ "$platform" == "Linux" ]]; then
160
- mkdir -p "$HOME/.local/bin"
161
- PATH="$HOME/.local/bin:$PATH"
162
- npm install --global --prefix "$HOME/.local" "$PACKAGE@$version" || fail "npm package installation failed."
82
+ prefix_args=(--prefix "$HOME/.local")
83
+ install_bin="$HOME/.local/bin/rea"
163
84
  else
164
- npm install --global "$PACKAGE@$version" || fail "npm package installation failed."
85
+ npm_prefix="$(npm prefix --global)" || fail "could not determine the npm global prefix."
86
+ install_bin="$npm_prefix/bin/rea"
165
87
  fi
166
- command -v rea >/dev/null 2>&1 || fail "the rea command is not available after installation."
167
- installed_version="$(rea --version 2>/dev/null | tr -d '[:space:]')" || fail "could not read the installed REA version."
168
- [[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version."
169
88
 
170
- temporary_directory="$(mktemp -d "${TMPDIR:-/tmp}/rea-install.XXXXXX")" || fail "could not create a temporary directory."
171
- setup_output="$temporary_directory/setup.json"
172
- doctor_output="$temporary_directory/doctor.json"
173
- rea setup --yes --json >"$setup_output" || fail "REA setup command failed."
174
- rea doctor --json >"$doctor_output" || fail "REA doctor command failed."
175
- result="$(node - "$setup_output" "$doctor_output" <<'NODE'
176
- const fs = require("node:fs");
177
- const [setupPath, doctorPath] = process.argv.slice(2);
178
- try {
179
- const setup = JSON.parse(fs.readFileSync(setupPath, "utf8"));
180
- const doctor = JSON.parse(fs.readFileSync(doctorPath, "utf8"));
181
- if (!Array.isArray(doctor.checks) || typeof doctor.healthy !== "boolean") process.exit(2);
182
- if (setup.status !== "ready" && setup.status !== "needs_human") process.exit(2);
183
- const activation = setup.actions.includes("installed_hopper");
184
- if (!doctor.healthy && !activation) process.exit(3);
185
- process.stdout.write(activation ? "activation" : "ready");
186
- } catch { process.exit(2); }
187
- NODE
188
- )" || fail "setup or doctor reported an unhealthy required component or malformed output."
89
+ printf 'REA install plan\n'
90
+ printf ' Version: %s\n' "$version"
91
+ printf ' Runtime: Node.js %s\n' "$node_version"
92
+ printf ' Command: npm install --global %s@%s\n' "$PACKAGE" "$version"
93
+ printf ' Binary: %s\n' "$install_bin"
94
+ printf ' Setup: %s\n' "$([[ "$start_setup" == true ]] && printf 'start when a terminal is available' || printf 'skipped')"
189
95
 
190
- if [[ "$result" == "activation" ]]; then
191
- printf 'REA %s is installed and coding agents are configured.\n' "$version"
192
- printf 'One user action remains: open Hopper, complete its activation, then run: rea doctor --json\n'
193
- else
194
- printf 'REA %s is installed and ready. Run rea doctor --json at any time.\n' "$version"
96
+ if [[ "$dry_run" == true ]]; then
97
+ printf 'Dry run complete; no changes made.\n'
98
+ exit 0
195
99
  fi
196
- if [[ "$platform" == "Linux" ]]; then
197
- printf 'Add %s to PATH to run rea from future shells.\n' "$HOME/.local/bin"
100
+
101
+ printf 'Installing %s@%s...\n' "$PACKAGE" "$version"
102
+ npm install --global "${prefix_args[@]}" "$PACKAGE@$version" || fail "npm package installation failed."
103
+ [[ -x "$install_bin" ]] || fail "the rea command was not installed at $install_bin."
104
+ installed_version="$("$install_bin" --version 2>/dev/null | tr -d '[:space:]')" || fail "could not read the installed REA version."
105
+ [[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version."
106
+
107
+ printf 'REA %s is installed.\n' "$version"
108
+ if [[ "$start_setup" == true && -r /dev/tty && -w /dev/tty && -t 1 ]]; then
109
+ printf 'Starting REA setup...\n'
110
+ "$install_bin" setup </dev/tty >/dev/tty
111
+ else
112
+ printf 'Run %s setup to configure Hopper and coding agents.\n' "$install_bin"
198
113
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rea-agents",
3
- "version": "1.0.0",
3
+ "version": "1.1.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": {
@@ -33,8 +33,7 @@
33
33
  "rea": "scripts/rea.mjs"
34
34
  },
35
35
  "engines": {
36
- "node": ">=24.18.0 <25",
37
- "npm": ">=11.16.0 <12"
36
+ "node": "^22.19.0 || >=24.11.0"
38
37
  },
39
38
  "packageManager": "npm@11.16.0",
40
39
  "files": [
@@ -99,7 +98,7 @@
99
98
  "pino": "9.14.0",
100
99
  "plist": "5.0.0",
101
100
  "smol-toml": "^1.7.0",
102
- "write-file-atomic": "8.0.0",
101
+ "write-file-atomic": "7.0.0",
103
102
  "ws": "8.21.0",
104
103
  "zod": "4.4.3"
105
104
  },