rea-agents 1.2.0 → 1.3.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.
Files changed (79) hide show
  1. package/README.md +74 -27
  2. package/bridge/hopper_bridge.py +161 -12
  3. package/dist/application/AnalysisSnapshotCache.js +131 -0
  4. package/dist/application/AnalysisSnapshotFiles.js +31 -0
  5. package/dist/application/ArtifactInventory.js +4 -0
  6. package/dist/application/AuthorizedArtifactInventory.js +16 -0
  7. package/dist/application/BinarySession.js +66 -36
  8. package/dist/application/BinarySessionPort.js +1 -0
  9. package/dist/application/BoundedJsonFiles.js +109 -0
  10. package/dist/application/CrossVersionInventory.js +98 -0
  11. package/dist/application/CrossVersionInvestigation.js +295 -0
  12. package/dist/application/DirectAnalysis.js +115 -22
  13. package/dist/application/Doctor.js +30 -1
  14. package/dist/application/EvidenceBundleFiles.js +15 -108
  15. package/dist/application/EvidenceLedger.js +3 -2
  16. package/dist/application/InvestigationProviders.js +48 -0
  17. package/dist/application/InvestigationWorkspaceStore.js +214 -0
  18. package/dist/application/LinuxHopper.js +72 -48
  19. package/dist/application/ProcessCaptureAuthority.js +2 -2
  20. package/dist/application/ProcessCaptureError.js +12 -0
  21. package/dist/application/ProcessCaptureLifecycle.js +19 -1
  22. package/dist/application/ProcessCli.js +98 -31
  23. package/dist/application/ProcessHarness.js +2 -2
  24. package/dist/application/ReferenceSourceImportEntries.js +18 -3
  25. package/dist/application/ReferenceSourceImportTypes.js +32 -0
  26. package/dist/application/Setup.js +33 -60
  27. package/dist/application/SetupClients.js +19 -0
  28. package/dist/application/SetupInstallFailure.js +53 -0
  29. package/dist/application/SetupPlan.js +31 -0
  30. package/dist/application/Uninstall.js +33 -23
  31. package/dist/application/UnknownEvidence.js +33 -0
  32. package/dist/application/Upgrade.js +1 -1
  33. package/dist/application/runtime.js +1 -1
  34. package/dist/artifacts/ArtifactProvider.js +2 -5
  35. package/dist/cli.js +31 -11
  36. package/dist/cliEvidenceCommands.js +11 -12
  37. package/dist/cliInvestigationCommands.js +86 -0
  38. package/dist/cliOutput.js +41 -0
  39. package/dist/cliProcessCommands.js +14 -4
  40. package/dist/config.js +39 -24
  41. package/dist/contracts/promptContracts.js +256 -0
  42. package/dist/contracts/sessionLifecycleInputs.js +11 -0
  43. package/dist/contracts/toolContractTypes.js +1 -0
  44. package/dist/contracts/toolContracts.js +10 -6
  45. package/dist/contracts/toolOutputSchemas.js +33 -4
  46. package/dist/domain/analysisSnapshot.js +149 -0
  47. package/dist/domain/changedBehavior.js +30 -7
  48. package/dist/domain/errors.js +180 -41
  49. package/dist/domain/evidence.js +4 -2
  50. package/dist/domain/evidenceBundle.js +28 -0
  51. package/dist/domain/hopperStartupFailure.js +55 -0
  52. package/dist/domain/investigationWorkspace.js +214 -0
  53. package/dist/domain/jsonValue.js +2 -0
  54. package/dist/domain/nativeInspection.js +3 -2
  55. package/dist/domain/processCapture.js +5 -4
  56. package/dist/domain/processComparison.js +5 -4
  57. package/dist/hopper/BridgeLauncher.js +47 -20
  58. package/dist/hopper/HopperClient.js +17 -2
  59. package/dist/hopper/HopperProvider.js +1 -0
  60. package/dist/main.js +47 -24
  61. package/dist/server/createServer.js +2 -0
  62. package/dist/server/promptCompletion.js +144 -0
  63. package/dist/server/registerEnhancedTools.js +2 -6
  64. package/dist/server/registerEvidenceTools.js +2 -8
  65. package/dist/server/registerFunctionComparisonTool.js +1 -1
  66. package/dist/server/registerInvestigationTools.js +31 -11
  67. package/dist/server/registerOfficialTools.js +6 -13
  68. package/dist/server/registerProcessComparisonTool.js +1 -7
  69. package/dist/server/registerPrompts.js +67 -0
  70. package/dist/server/registerSessionStatusTool.js +11 -0
  71. package/dist/server/registerSessionTools.js +52 -18
  72. package/dist/server/sessionEvidence.js +2 -8
  73. package/dist/server/sessionToolPolicies.js +1 -42
  74. package/dist/server/toolResult.js +7 -3
  75. package/install.sh +11 -11
  76. package/package.json +5 -3
  77. package/scripts/hopper-demo-x11.py +349 -0
  78. package/scripts/rea.mjs +6 -3
  79. package/skills/rea-analysis/SKILL.md +9 -2
@@ -1,4 +1,4 @@
1
- import { projectAnalysisError } from "../domain/errors.js";
1
+ import { AnalysisOutputError, projectAnalysisError, } from "../domain/errors.js";
2
2
  /**
3
3
  * Translate an application result into MCP text content.
4
4
  * Error tags and safe messages remain visible while underlying causes, process
@@ -11,9 +11,10 @@ const errorResult = (error) => {
11
11
  content: [
12
12
  {
13
13
  type: "text",
14
- text: `${projected.tag}: ${projected.message}`,
14
+ text: projected.message,
15
15
  },
16
16
  ],
17
+ structuredContent: { error: projected },
17
18
  isError: true,
18
19
  };
19
20
  };
@@ -25,9 +26,12 @@ const successResult = (value, contract) => {
25
26
  content: [
26
27
  {
27
28
  type: "text",
28
- text: "AnalysisOutputError: Analysis output does not match the tool contract",
29
+ text: "Analysis returned an unreadable result. Retry once; if it continues, run `rea doctor`.",
29
30
  },
30
31
  ],
32
+ structuredContent: {
33
+ error: projectAnalysisError(new AnalysisOutputError(contract.name, "output does not match the tool contract")),
34
+ },
31
35
  isError: true,
32
36
  };
33
37
  }
package/install.sh CHANGED
@@ -17,7 +17,7 @@ usage() {
17
17
  cat <<'EOF'
18
18
  Usage: install.sh [--version VERSION] [--dry-run] [--no-setup] [--no-prompt] [--verbose]
19
19
 
20
- Installs only the REA CLI. Run `rea setup` separately to configure Hopper and coding agents.
20
+ Installs only the REA CLI. Run `rea setup` separately to configure Hopper and agents.
21
21
  EOF
22
22
  }
23
23
 
@@ -54,13 +54,13 @@ fi
54
54
 
55
55
  platform="$(uname -s)"
56
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."
57
+ command -v curl >/dev/null 2>&1 || fail "curl is required. Install curl, then rerun this installer."
58
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
59
  command -v npm >/dev/null 2>&1 || fail "npm is required; install it with Node.js and retry."
60
60
 
61
61
  node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
62
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."
63
+ [[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "the active Node.js version could not be read. Check that node works and is on PATH, then retry."
64
64
  if ! ((node_major == 22 && node_minor >= 19 || node_major >= 24)); then
65
65
  fail "Node.js $node_version is unsupported; use Node.js 22.19+ or 24.11+."
66
66
  fi
@@ -71,9 +71,9 @@ fi
71
71
  if [[ -n "$version" ]]; then
72
72
  [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || fail "version must be an exact semantic version."
73
73
  else
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."
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."
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."
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 "the latest REA release could not be resolved. Check network access or pass --version VERSION, then retry."
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)})' 2>/dev/null)" || fail "the release response was invalid. Retry later or pass --version VERSION."
76
+ [[ "$tag" =~ ^rea-agents-([0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?)$ ]] || fail "the latest release tag was invalid. Retry later or pass --version VERSION."
77
77
  version="${BASH_REMATCH[1]}"
78
78
  fi
79
79
 
@@ -82,7 +82,7 @@ if [[ "$platform" == "Linux" ]]; then
82
82
  prefix_args=(--prefix "$HOME/.local")
83
83
  install_bin="$HOME/.local/bin/rea"
84
84
  else
85
- npm_prefix="$(npm prefix --global)" || fail "could not determine the npm global prefix."
85
+ npm_prefix="$(npm prefix --global)" || fail "the npm global prefix could not be read. Repair the npm configuration, then retry."
86
86
  install_bin="$npm_prefix/bin/rea"
87
87
  fi
88
88
 
@@ -99,9 +99,9 @@ if [[ "$dry_run" == true ]]; then
99
99
  fi
100
100
 
101
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."
102
+ npm install --global "${prefix_args[@]}" "$PACKAGE@$version" || fail "npm could not install REA. Check registry access and npm permissions, then retry."
103
+ [[ -x "$install_bin" ]] || fail "npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry."
104
+ installed_version="$("$install_bin" --version 2>/dev/null | tr -d '[:space:]')" || fail "the installed REA version could not be read. Reinstall the requested version, then retry."
105
105
  [[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version."
106
106
 
107
107
  printf 'REA %s is installed.\n' "$version"
@@ -109,5 +109,5 @@ if [[ "$start_setup" == true && -r /dev/tty && -w /dev/tty && -t 1 ]]; then
109
109
  printf 'Starting REA setup...\n'
110
110
  "$install_bin" setup </dev/tty >/dev/tty
111
111
  else
112
- printf 'Run %s setup to configure Hopper and coding agents.\n' "$install_bin"
112
+ printf 'Run %s setup to configure Hopper and agents.\n' "$install_bin"
113
113
  fi
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rea-agents",
3
- "version": "1.2.0",
4
- "description": "Reverse engineer anything from your terminal or coding agent with one CLI and MCP server.",
3
+ "version": "1.3.0",
4
+ "description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -14,7 +14,7 @@
14
14
  "keywords": [
15
15
  "reverse-engineering",
16
16
  "binary-analysis",
17
- "coding-agents",
17
+ "agents",
18
18
  "agent-skills",
19
19
  "cli",
20
20
  "mcp-server",
@@ -43,6 +43,7 @@
43
43
  "scripts/rea.mjs",
44
44
  "scripts/prepare-node-pty.mjs",
45
45
  "scripts/rebuild-native.mjs",
46
+ "scripts/hopper-demo-x11.py",
46
47
  "skills",
47
48
  "README.md",
48
49
  "LICENSE"
@@ -69,6 +70,7 @@
69
70
  "typecheck": "tsc --noEmit",
70
71
  "verify:hopper": "npm run build && node scripts/verify-real-hopper.mjs",
71
72
  "verify:hopper:linux": "npm run build && node scripts/verify-real-hopper-linux.mjs",
73
+ "verify:setup:linux": "npm run build && node scripts/verify-linux-setup-e2e.mjs",
72
74
  "build:fixtures": "node scripts/build-conformance-fixtures.mjs",
73
75
  "verify:fixtures": "npm run build:fixtures && node scripts/verify-conformance-fixtures.mjs",
74
76
  "verify:package": "npm run build && node scripts/verify-package.mjs",
@@ -0,0 +1,349 @@
1
+ #!/usr/bin/env python3
2
+ """Inspect or activate Hopper's version-pinned demo dialog on a private X display."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import ctypes
8
+ import hashlib
9
+ import os
10
+ from pathlib import Path
11
+ import secrets
12
+ import subprocess
13
+ import time
14
+
15
+
16
+ # Keep this digest aligned with LinuxHopper.ts and rerun real-Hopper verification
17
+ # before accepting another vendor build.
18
+ SUPPORTED_HOPPER_SHA256 = {
19
+ "0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded"
20
+ }
21
+ EXPECTED_SCREEN = (1280, 1024)
22
+ EXPECTED_DIALOG = (189, 370, 901, 284)
23
+ DEMO_CLICK = (305, 632)
24
+ PRIVATE_DISPLAY_UNAVAILABLE = 70
25
+ X11_AUTHORIZATION_FAILED = 71
26
+ UNSUPPORTED_HOPPER_BUILD = 72
27
+ INVALID_LAUNCH_COMMAND = 73
28
+ PROCESS_OWNERSHIP_MISMATCH = 74
29
+ HOPPER_EXITED_DURING_STARTUP = 75
30
+ UNSUPPORTED_DEMO_DIALOG = 76
31
+ UNEXPECTED_DISPLAY_GEOMETRY = 77
32
+ X11_INPUT_FAILED = 78
33
+ RUNTIME_DEPENDENCY_UNAVAILABLE = 79
34
+
35
+
36
+ class AdapterFailure(Exception):
37
+ def __init__(self, code: int, message: str) -> None:
38
+ super().__init__(message)
39
+ self.code = code
40
+
41
+
42
+ class XWindowAttributes(ctypes.Structure):
43
+ _fields_ = [
44
+ ("x", ctypes.c_int),
45
+ ("y", ctypes.c_int),
46
+ ("width", ctypes.c_int),
47
+ ("height", ctypes.c_int),
48
+ ("border_width", ctypes.c_int),
49
+ ("depth", ctypes.c_int),
50
+ ("visual", ctypes.c_void_p),
51
+ ("root", ctypes.c_ulong),
52
+ ("class_", ctypes.c_int),
53
+ ("bit_gravity", ctypes.c_int),
54
+ ("win_gravity", ctypes.c_int),
55
+ ("backing_store", ctypes.c_int),
56
+ ("backing_planes", ctypes.c_ulong),
57
+ ("backing_pixel", ctypes.c_ulong),
58
+ ("save_under", ctypes.c_int),
59
+ ("colormap", ctypes.c_ulong),
60
+ ("map_installed", ctypes.c_int),
61
+ ("map_state", ctypes.c_int),
62
+ ("all_event_masks", ctypes.c_long),
63
+ ("your_event_mask", ctypes.c_long),
64
+ ("do_not_propagate_mask", ctypes.c_long),
65
+ ("override_redirect", ctypes.c_int),
66
+ ("screen", ctypes.c_void_p),
67
+ ]
68
+
69
+
70
+ def parse_args() -> argparse.Namespace:
71
+ parser = argparse.ArgumentParser()
72
+ parser.add_argument("--hopper", required=True)
73
+ parser.add_argument("--socket", required=True)
74
+ parser.add_argument("command", nargs=argparse.REMAINDER)
75
+ return parser.parse_args()
76
+
77
+
78
+ def digest(path: Path) -> str:
79
+ value = hashlib.sha256()
80
+ with path.open("rb") as file:
81
+ for chunk in iter(lambda: file.read(1024 * 1024), b""):
82
+ value.update(chunk)
83
+ return value.hexdigest()
84
+
85
+
86
+ def descendants(pid: int) -> set[int]:
87
+ found = {pid}
88
+ changed = True
89
+ while changed:
90
+ changed = False
91
+ for stat in Path("/proc").glob("[0-9]*/stat"):
92
+ try:
93
+ fields = stat.read_text().split()
94
+ child = int(fields[0])
95
+ parent = int(fields[3])
96
+ except (OSError, ValueError, IndexError):
97
+ continue
98
+ if parent in found and child not in found:
99
+ found.add(child)
100
+ changed = True
101
+ return found
102
+
103
+
104
+ def hopper_is_owned(leader: int, hopper: Path) -> bool:
105
+ expected = hopper.resolve()
106
+ for pid in descendants(leader):
107
+ try:
108
+ if Path(f"/proc/{pid}/exe").resolve() == expected:
109
+ return True
110
+ except OSError:
111
+ continue
112
+ return False
113
+
114
+
115
+ def windows(x11: ctypes.CDLL, display: int, root: int) -> list[tuple[int, int, int, int, int]]:
116
+ found: list[tuple[int, int, int, int, int]] = []
117
+ pending = [root]
118
+ while pending:
119
+ window = pending.pop()
120
+ attributes = XWindowAttributes()
121
+ if x11.XGetWindowAttributes(display, window, ctypes.byref(attributes)):
122
+ if attributes.map_state == 2:
123
+ found.append(
124
+ (window, attributes.x, attributes.y, attributes.width, attributes.height)
125
+ )
126
+ root_return = ctypes.c_ulong()
127
+ parent_return = ctypes.c_ulong()
128
+ children = ctypes.POINTER(ctypes.c_ulong)()
129
+ count = ctypes.c_uint()
130
+ if x11.XQueryTree(
131
+ display,
132
+ window,
133
+ ctypes.byref(root_return),
134
+ ctypes.byref(parent_return),
135
+ ctypes.byref(children),
136
+ ctypes.byref(count),
137
+ ):
138
+ pending.extend(children[index] for index in range(count.value))
139
+ if children:
140
+ x11.XFree(children)
141
+ return found
142
+
143
+
144
+ def configure_x11(x11: ctypes.CDLL, xtst: ctypes.CDLL) -> None:
145
+ window_pointer = ctypes.POINTER(ctypes.c_ulong)
146
+ x11.XOpenDisplay.argtypes = [ctypes.c_char_p]
147
+ x11.XOpenDisplay.restype = ctypes.c_void_p
148
+ x11.XDefaultRootWindow.argtypes = [ctypes.c_void_p]
149
+ x11.XDefaultRootWindow.restype = ctypes.c_ulong
150
+ x11.XDisplayWidth.argtypes = [ctypes.c_void_p, ctypes.c_int]
151
+ x11.XDisplayWidth.restype = ctypes.c_int
152
+ x11.XDisplayHeight.argtypes = [ctypes.c_void_p, ctypes.c_int]
153
+ x11.XDisplayHeight.restype = ctypes.c_int
154
+ x11.XGetWindowAttributes.argtypes = [
155
+ ctypes.c_void_p,
156
+ ctypes.c_ulong,
157
+ ctypes.POINTER(XWindowAttributes),
158
+ ]
159
+ x11.XGetWindowAttributes.restype = ctypes.c_int
160
+ x11.XQueryTree.argtypes = [
161
+ ctypes.c_void_p,
162
+ ctypes.c_ulong,
163
+ window_pointer,
164
+ window_pointer,
165
+ ctypes.POINTER(window_pointer),
166
+ ctypes.POINTER(ctypes.c_uint),
167
+ ]
168
+ x11.XQueryTree.restype = ctypes.c_int
169
+ x11.XFree.argtypes = [ctypes.c_void_p]
170
+ x11.XFree.restype = ctypes.c_int
171
+ x11.XFlush.argtypes = [ctypes.c_void_p]
172
+ x11.XFlush.restype = ctypes.c_int
173
+ x11.XCloseDisplay.argtypes = [ctypes.c_void_p]
174
+ x11.XCloseDisplay.restype = ctypes.c_int
175
+ xtst.XTestFakeMotionEvent.argtypes = [
176
+ ctypes.c_void_p,
177
+ ctypes.c_int,
178
+ ctypes.c_int,
179
+ ctypes.c_int,
180
+ ctypes.c_ulong,
181
+ ]
182
+ xtst.XTestFakeMotionEvent.restype = ctypes.c_int
183
+ xtst.XTestFakeButtonEvent.argtypes = [
184
+ ctypes.c_void_p,
185
+ ctypes.c_uint,
186
+ ctypes.c_int,
187
+ ctypes.c_ulong,
188
+ ]
189
+ xtst.XTestFakeButtonEvent.restype = ctypes.c_int
190
+
191
+
192
+ def start_xvfb(session: Path) -> tuple[subprocess.Popen[bytes], str, Path]:
193
+ authority = session / "Xauthority"
194
+ for _ in range(20):
195
+ display_number = 100 + secrets.randbelow(900)
196
+ display_name = f":{display_number}"
197
+ if Path(f"/tmp/.X11-unix/X{display_number}").exists():
198
+ continue
199
+ cookie = secrets.token_hex(16)
200
+ try:
201
+ subprocess.run(
202
+ [
203
+ "/usr/bin/xauth", "-f", str(authority), "add",
204
+ display_name, ".", cookie,
205
+ ],
206
+ check=True,
207
+ stdout=subprocess.DEVNULL,
208
+ stderr=subprocess.DEVNULL,
209
+ )
210
+ except (OSError, subprocess.CalledProcessError) as error:
211
+ raise AdapterFailure(
212
+ X11_AUTHORIZATION_FAILED, "private X authorization failed"
213
+ ) from error
214
+ authority.chmod(0o600)
215
+ process = subprocess.Popen(
216
+ [
217
+ "/usr/bin/Xvfb",
218
+ display_name,
219
+ "-screen",
220
+ "0",
221
+ "1280x1024x24",
222
+ "-nolisten",
223
+ "tcp",
224
+ "-auth",
225
+ str(authority),
226
+ ],
227
+ stdout=subprocess.DEVNULL,
228
+ stderr=subprocess.DEVNULL,
229
+ )
230
+ deadline = time.monotonic() + 2
231
+ socket = Path(f"/tmp/.X11-unix/X{display_number}")
232
+ while time.monotonic() < deadline and process.poll() is None:
233
+ if socket.exists():
234
+ return process, display_name, authority
235
+ time.sleep(0.05)
236
+ if process.poll() is None:
237
+ process.terminate()
238
+ process.wait()
239
+ raise AdapterFailure(PRIVATE_DISPLAY_UNAVAILABLE, "private X display unavailable")
240
+
241
+
242
+ def main() -> int:
243
+ args = parse_args()
244
+ hopper = Path(args.hopper)
245
+ socket = Path(args.socket)
246
+ if digest(hopper) not in SUPPORTED_HOPPER_SHA256:
247
+ raise AdapterFailure(UNSUPPORTED_HOPPER_BUILD, "unsupported Hopper binary")
248
+ command = args.command[1:] if args.command[:1] == ["--"] else args.command
249
+ if not command or Path(command[0]).resolve() != hopper.resolve():
250
+ raise AdapterFailure(INVALID_LAUNCH_COMMAND, "invalid Hopper launch command")
251
+ xvfb, display_name, authority = start_xvfb(socket.parent)
252
+ child: subprocess.Popen[bytes] | None = None
253
+ display: int | None = None
254
+ try:
255
+ hopper_environment = dict(os.environ)
256
+ for name in ("WAYLAND_DISPLAY", "WAYLAND_SOCKET"):
257
+ hopper_environment.pop(name, None)
258
+ hopper_environment.update(
259
+ {
260
+ "DISPLAY": display_name,
261
+ "XAUTHORITY": str(authority),
262
+ "GDK_BACKEND": "x11",
263
+ "QT_QPA_PLATFORM": "xcb",
264
+ "XDG_SESSION_TYPE": "x11",
265
+ }
266
+ )
267
+ child = subprocess.Popen(command, env=hopper_environment)
268
+ leader = os.getpid()
269
+ os.environ["XAUTHORITY"] = str(authority)
270
+
271
+ try:
272
+ x11 = ctypes.CDLL("libX11.so.6")
273
+ xtst = ctypes.CDLL("libXtst.so.6")
274
+ except OSError as error:
275
+ raise AdapterFailure(
276
+ RUNTIME_DEPENDENCY_UNAVAILABLE, "X11 runtime unavailable"
277
+ ) from error
278
+ configure_x11(x11, xtst)
279
+ display = x11.XOpenDisplay(display_name.encode())
280
+ if not display:
281
+ raise AdapterFailure(PRIVATE_DISPLAY_UNAVAILABLE, "private X display unavailable")
282
+ root = x11.XDefaultRootWindow(display)
283
+ if (
284
+ x11.XDisplayWidth(display, 0),
285
+ x11.XDisplayHeight(display, 0),
286
+ ) != EXPECTED_SCREEN:
287
+ raise AdapterFailure(
288
+ UNEXPECTED_DISPLAY_GEOMETRY, "unexpected private display geometry"
289
+ )
290
+
291
+ deadline = time.monotonic() + 10
292
+ observed: list[tuple[int, int, int, int, int]] = []
293
+ while time.monotonic() < deadline:
294
+ if socket.exists():
295
+ x11.XCloseDisplay(display)
296
+ display = None
297
+ return child.wait()
298
+ if not hopper_is_owned(leader, hopper):
299
+ if child.poll() is None:
300
+ time.sleep(0.1)
301
+ continue
302
+ raise AdapterFailure(
303
+ HOPPER_EXITED_DURING_STARTUP, "owned Hopper process exited"
304
+ )
305
+ observed = windows(x11, display, root)
306
+ if any(window[1:] == EXPECTED_DIALOG for window in observed):
307
+ break
308
+ time.sleep(0.1)
309
+ else:
310
+ if not hopper_is_owned(leader, hopper):
311
+ raise AdapterFailure(
312
+ PROCESS_OWNERSHIP_MISMATCH, "Hopper process ownership mismatch"
313
+ )
314
+ raise AdapterFailure(
315
+ UNSUPPORTED_DEMO_DIALOG, "expected Hopper demo dialog not found"
316
+ )
317
+
318
+ input_results = (
319
+ xtst.XTestFakeMotionEvent(display, -1, DEMO_CLICK[0], DEMO_CLICK[1], 0),
320
+ xtst.XTestFakeButtonEvent(display, 1, True, 0),
321
+ xtst.XTestFakeButtonEvent(display, 1, False, 0),
322
+ )
323
+ if not all(input_results):
324
+ raise AdapterFailure(X11_INPUT_FAILED, "private X input failed")
325
+ x11.XFlush(display)
326
+ x11.XCloseDisplay(display)
327
+ display = None
328
+ return child.wait()
329
+ finally:
330
+ if display is not None:
331
+ x11.XCloseDisplay(display)
332
+ if child is not None and child.poll() is None:
333
+ child.terminate()
334
+ child.wait()
335
+ if xvfb.poll() is None:
336
+ xvfb.terminate()
337
+ try:
338
+ xvfb.wait(timeout=2)
339
+ except subprocess.TimeoutExpired:
340
+ xvfb.kill()
341
+ xvfb.wait()
342
+
343
+
344
+ if __name__ == "__main__":
345
+ try:
346
+ raise SystemExit(main())
347
+ except AdapterFailure as error:
348
+ print(str(error), file=os.sys.stderr)
349
+ raise SystemExit(error.code) from None
package/scripts/rea.mjs CHANGED
@@ -11,9 +11,12 @@ const isMcpMode =
11
11
  args[0] === "--mcp" || (args.length === 1 && args[0] === "mcp");
12
12
 
13
13
  if (isMcpMode) {
14
- const { run } = await import("../dist/main.js");
15
- process.exitCode = await run();
14
+ const { runEntrypoint } = await import("../dist/main.js");
15
+ await runEntrypoint();
16
16
  } else {
17
17
  const { createCli } = await import("../dist/cli.js");
18
- await createCli().serve(args);
18
+ const { sanitizeCliOutput } = await import("../dist/cliOutput.js");
19
+ await createCli().serve(args, {
20
+ stdout: (output) => process.stdout.write(sanitizeCliOutput(output)),
21
+ });
19
22
  }
@@ -2,7 +2,7 @@
2
2
  name: rea-analysis
3
3
  description: Reverse engineer apps with REA. Explore how features work, then build a version tailored to your project.
4
4
  metadata:
5
- version: "11"
5
+ version: "12"
6
6
  tool_count: 68
7
7
  ---
8
8
 
@@ -24,7 +24,7 @@ Notes is only a documentation example. Never select an app unless the user names
24
24
  2. If setup is needed, tell the user REA needs to install its local binary-analysis tools. Do not lead with implementation details or assume the user knows reverse-engineering products.
25
25
  3. Before installing external software, obtain approval and identify what will be installed. If deeper analysis needs Hopper, describe it as REA's local analysis engine and note that it is a separate Mac app with its own license. Then run `npx -y rea-agents setup --yes`.
26
26
  4. If macOS or an installer requests human input, tell the user exactly what needs attention. After they finish, rerun setup and doctor.
27
- 5. If setup registers a new MCP server, tell the user to restart their coding agent to load all REA tools. Direct CLI commands remain available before restart.
27
+ 5. If setup registers a new MCP server, tell the user to restart their agent to load all REA tools. Direct CLI commands remain available before restart.
28
28
 
29
29
  ## Locate the app
30
30
 
@@ -109,6 +109,13 @@ bundle digests to anchor paginated reports.
109
109
  Use `find_changed_behavior` to combine existing comparison Evidence. Runtime
110
110
  process differences are observed changes; artifact and function differences
111
111
  remain static candidates, not causal proof. Supply complete comparison pages.
112
+ For an automatic two-version artifact investigation, use its
113
+ `investigation_run` mode with explicit write approval and a workspace beneath
114
+ `REA_EVIDENCE_ROOTS_JSON`. It checkpoints inventory, comparison, and report
115
+ Evidence in monotonic CAS-linked revisions. Repeating the same content and
116
+ budgets resumes or reuses the run. This automatic mode does not execute either
117
+ version or perform fuzzy function matching, so its behavior status remains
118
+ unknown without separate controlled runtime Evidence.
112
119
 
113
120
  Use `build_call_path` with explicit `analyze_function` Evidence groups from one
114
121
  artifact and provider. Select endpoints by exact address. Missing dossiers,