rea-agents 2.0.0 → 2.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.
Files changed (246) hide show
  1. package/README.md +78 -32
  2. package/bridge/ghidra/ReaGhidraBridge.java +96 -11
  3. package/dist/application/ArtifactExtraction.js +1 -1
  4. package/dist/application/ArtifactInventory/classify.js +42 -0
  5. package/dist/application/ArtifactInventory/hash.js +43 -0
  6. package/dist/application/ArtifactInventory/reader.js +41 -0
  7. package/dist/application/ArtifactInventory/scanCanonical.js +155 -0
  8. package/dist/application/ArtifactInventory/scanReader.js +124 -0
  9. package/dist/application/ArtifactInventory/types.js +10 -0
  10. package/dist/application/ArtifactInventory.js +9 -351
  11. package/dist/application/AuthorizedArtifactInventory.js +2 -2
  12. package/dist/application/BinarySession.js +47 -270
  13. package/dist/application/BinarySessionExecution.js +28 -0
  14. package/dist/application/BinarySessionOpen.js +39 -0
  15. package/dist/application/BinarySessionRecords.js +149 -0
  16. package/dist/application/BinarySessionStatus.js +75 -0
  17. package/dist/application/CapabilityInventory.js +24 -2
  18. package/dist/application/ClientRegistrationStatus.js +1 -1
  19. package/dist/application/CrossVersionInventory.js +8 -2
  20. package/dist/application/CrossVersionInvestigation.js +1 -23
  21. package/dist/application/CrossVersionInvestigationCollections.js +23 -0
  22. package/dist/application/DirectAnalysis.js +47 -44
  23. package/dist/application/Doctor.js +156 -238
  24. package/dist/application/DoctorDiagnostics.js +180 -0
  25. package/dist/application/ElectronBoundaryAnalysis.js +23 -7
  26. package/dist/application/ElectronBoundaryGraphWindows.js +32 -8
  27. package/dist/application/EnhancedToolPagination.js +22 -0
  28. package/dist/application/EnhancedToolTypes.js +4 -0
  29. package/dist/application/EnhancedTools.js +20 -37
  30. package/dist/application/InstrumentedJavaScriptReplayHost.js +36 -0
  31. package/dist/application/InvestigationWorkspaceStore.js +24 -10
  32. package/dist/application/JavaScriptArtifactAnalysis.js +45 -6
  33. package/dist/application/JavaScriptArtifactFiles.js +6 -2
  34. package/dist/application/JavaScriptArtifactGraphBuilder.js +23 -1
  35. package/dist/application/JavaScriptArtifactGraphBundlers.js +225 -0
  36. package/dist/application/JavaScriptArtifactGraphContext.js +37 -36
  37. package/dist/application/JavaScriptArtifactGraphDocuments.js +27 -4
  38. package/dist/application/JavaScriptArtifactGraphFindings.js +54 -8
  39. package/dist/application/JavaScriptArtifactGraphStructure.js +29 -101
  40. package/dist/application/JavaScriptArtifactPathResolution.js +269 -0
  41. package/dist/application/JavaScriptArtifactReconstruction.js +1 -1
  42. package/dist/application/JavaScriptJsonModules.js +39 -0
  43. package/dist/application/JavaScriptModuleRelationships.js +262 -0
  44. package/dist/application/JavaScriptReplayPermission.js +16 -0
  45. package/dist/application/JavaScriptReplayService.js +182 -156
  46. package/dist/application/LinuxHopper.js +10 -0
  47. package/dist/application/MacHopper.js +22 -11
  48. package/dist/application/NodeRuntimeCharacterizationService.js +196 -0
  49. package/dist/application/PermissionAuthority.js +106 -63
  50. package/dist/application/ProcessCaptureCapability.js +1 -1
  51. package/dist/application/ProcessCapturePermission.js +13 -0
  52. package/dist/application/ProcessCli.js +2 -12
  53. package/dist/application/ProcessHarness.js +1 -1
  54. package/dist/application/ProjectPermissionStore.js +48 -9
  55. package/dist/application/RealHopperAssertions.js +76 -0
  56. package/dist/application/ReconstructionCoverageService.js +69 -0
  57. package/dist/application/ReconstructionCoverageWorkspaceStore.js +31 -0
  58. package/dist/application/RuntimeExecutableDiagnostics.js +172 -0
  59. package/dist/application/Setup.js +147 -259
  60. package/dist/application/SetupClientConfiguration.js +220 -0
  61. package/dist/application/SetupClients.js +24 -2
  62. package/dist/application/SetupPlan.js +123 -12
  63. package/dist/application/SetupSkill.js +26 -12
  64. package/dist/application/SupportedClients.js +1 -0
  65. package/dist/application/Uninstall.js +6 -3
  66. package/dist/application/WindowsCapabilities.js +86 -0
  67. package/dist/artifacts/AsarArtifactReader.js +11 -0
  68. package/dist/browser/BrowserProcessStartup.js +88 -0
  69. package/dist/browser/CdpCaptureDocuments.js +12 -8
  70. package/dist/browser/CdpCaptureEventBodyShapes.js +75 -0
  71. package/dist/browser/CdpCaptureEventHandlers.js +336 -0
  72. package/dist/browser/CdpCaptureEventHelpers.js +71 -0
  73. package/dist/browser/CdpCaptureEventState.js +1 -0
  74. package/dist/browser/CdpCaptureEventTypes.js +1 -0
  75. package/dist/browser/CdpCaptureEvents.js +29 -496
  76. package/dist/browser/CdpObservationSession.js +51 -8
  77. package/dist/browser/CdpPageCapture.js +12 -133
  78. package/dist/browser/CdpPageCaptureScripts.js +97 -0
  79. package/dist/browser/CdpPageCaptureWorkers.js +46 -0
  80. package/dist/browser/CdpWebMcpDiscovery.js +38 -14
  81. package/dist/browser/PngVisualDiff.js +9 -2
  82. package/dist/catalogIdentity.js +4 -0
  83. package/dist/cli/artifactCommands.js +82 -0
  84. package/dist/cli/coreAnalysisCommands.js +232 -0
  85. package/dist/cli/managedCommands.js +397 -0
  86. package/dist/cli/managedProjectGraphCommand.js +24 -0
  87. package/dist/cli/options.js +9 -0
  88. package/dist/cli/setupCommands.js +108 -0
  89. package/dist/cli/types.js +1 -0
  90. package/dist/cli/utilityCommands.js +122 -0
  91. package/dist/cli.js +10 -768
  92. package/dist/cliApplicationCommands.js +118 -4
  93. package/dist/cliBrowserAdvancedCommands.js +73 -16
  94. package/dist/cliBrowserCommands.js +39 -115
  95. package/dist/cliCommandNames.js +4 -0
  96. package/dist/cliElectronCommands.js +33 -62
  97. package/dist/cliEvidenceCommands.js +13 -2
  98. package/dist/cliInvestigationCommands.js +53 -10
  99. package/dist/cliObservationOptions.js +137 -0
  100. package/dist/cliPolicyCommands.js +80 -46
  101. package/dist/cliSetup.js +289 -0
  102. package/dist/config/environment.js +107 -0
  103. package/dist/config/parseConfig.js +228 -0
  104. package/dist/config/parsers.js +68 -0
  105. package/dist/config/permissions.js +30 -0
  106. package/dist/config/types.js +1 -0
  107. package/dist/config.js +1 -375
  108. package/dist/contracts/applicationToolContracts.js +134 -0
  109. package/dist/contracts/managedToolContracts.js +1 -1
  110. package/dist/contracts/managedWorkflowToolContracts.js +3 -3
  111. package/dist/contracts/providerSelection.js +2 -0
  112. package/dist/contracts/toolContracts.js +2 -2
  113. package/dist/contracts/toolEffects.js +14 -0
  114. package/dist/contracts/toolOutputSchemaGroups.js +232 -0
  115. package/dist/contracts/toolOutputSchemaPrimitives.js +327 -0
  116. package/dist/contracts/toolOutputSchemas.js +2 -550
  117. package/dist/domain/analysisErrorPresentation.js +234 -0
  118. package/dist/domain/analysisErrorProjection.js +284 -0
  119. package/dist/domain/artifactComparison.js +34 -15
  120. package/dist/domain/binaryTarget.js +87 -37
  121. package/dist/domain/browserObservation.js +1 -285
  122. package/dist/domain/browserObservationSchemas.js +286 -0
  123. package/dist/domain/electronStaticAnalysisBrowser.js +14 -2
  124. package/dist/domain/errors.js +4 -486
  125. package/dist/domain/functionComparison.js +4 -165
  126. package/dist/domain/functionComparisonDimensions.js +164 -0
  127. package/dist/domain/javascriptApplicationVersionItems.js +18 -6
  128. package/dist/domain/javascriptApplicationVersionKeys.js +8 -8
  129. package/dist/domain/javascriptExportInstrumentation.js +97 -0
  130. package/dist/domain/javascriptFeatureTraversal.js +19 -7
  131. package/dist/domain/javascriptRuntimeStaticCandidates.js +3 -1
  132. package/dist/domain/javascriptSemanticAnalysis.js +386 -0
  133. package/dist/domain/javascriptSemanticIr.js +30 -0
  134. package/dist/domain/javascriptSemanticProjection.js +366 -0
  135. package/dist/domain/javascriptSemanticState.js +40 -0
  136. package/dist/domain/javascriptSemanticTraversal.js +22 -0
  137. package/dist/domain/javascriptSemanticValues.js +352 -0
  138. package/dist/domain/javascriptStaticAnalysis.js +5 -221
  139. package/dist/domain/javascriptStaticAnalysisBundler.js +209 -0
  140. package/dist/domain/javascriptStaticAnalysisCalls.js +180 -0
  141. package/dist/domain/javascriptStaticAnalysisHelpers.js +71 -2
  142. package/dist/domain/knownPageCoverage.js +21 -0
  143. package/dist/domain/managedApplicationGraph.js +14 -364
  144. package/dist/domain/managedApplicationGraphCoverage.js +170 -0
  145. package/dist/domain/managedApplicationGraphNodeCoverage.js +6 -0
  146. package/dist/domain/managedApplicationGraphNodes.js +336 -0
  147. package/dist/domain/managedArtifact.js +16 -8
  148. package/dist/domain/managedMemberComparison.js +37 -391
  149. package/dist/domain/managedMemberComparisonCoverage.js +72 -0
  150. package/dist/domain/managedMemberComparisonMatch.js +356 -0
  151. package/dist/domain/managedNativeVerification.js +27 -389
  152. package/dist/domain/managedNativeVerificationMatch.js +272 -0
  153. package/dist/domain/managedNativeVerificationSchemas.js +141 -0
  154. package/dist/domain/managedReconstruction.js +82 -69
  155. package/dist/domain/managedRuntimeCorrelation.js +12 -3
  156. package/dist/domain/nodeRuntimeCharacterization.js +66 -0
  157. package/dist/domain/permissionPolicy.js +2 -0
  158. package/dist/domain/reconstructionCoverage.js +387 -0
  159. package/dist/domain/reconstructionCoverageEvaluation.js +212 -0
  160. package/dist/domain/reconstructionVerificationSchemas.js +8 -0
  161. package/dist/domain/runtimeCharacterization.js +85 -0
  162. package/dist/domain/webBundleAnalyzer.js +3 -409
  163. package/dist/domain/webBundleAnalyzerAst.js +77 -0
  164. package/dist/domain/webBundleAnalyzerInspection.js +307 -0
  165. package/dist/domain/webBundleAnalyzerResult.js +120 -0
  166. package/dist/dotnet/ManagedArtifactInspector.js +50 -29
  167. package/dist/dotnet/ManagedMemberInspector.js +51 -785
  168. package/dist/dotnet/ManagedMemberInspectorCore.js +297 -0
  169. package/dist/dotnet/ManagedMemberInstructionDecoder.js +427 -0
  170. package/dist/dotnet/ManagedMemberRows.js +120 -0
  171. package/dist/dotnet/ManagedMetadataInventory.js +42 -306
  172. package/dist/dotnet/ManagedMetadataInventoryRows.js +292 -0
  173. package/dist/dotnet/ManagedMetadataLayout.js +48 -111
  174. package/dist/dotnet/ManagedMetadataRowSizes.js +54 -0
  175. package/dist/dotnet/ManagedMethodBodyReader.js +143 -0
  176. package/dist/dotnet/ManagedNativeBoundaryHelpers.js +311 -0
  177. package/dist/dotnet/ManagedNativeBoundaryInspector.js +102 -350
  178. package/dist/dotnet/ManagedPeReader.js +59 -28
  179. package/dist/generatedPackageMetadata.js +2 -2
  180. package/dist/ghidra/GhidraClient.js +76 -91
  181. package/dist/ghidra/GhidraClientStartup.js +29 -0
  182. package/dist/ghidra/GhidraClientWire.js +34 -0
  183. package/dist/ghidra/GhidraDefaults.js +1 -1
  184. package/dist/ghidra/GhidraDiagnostics.js +6 -1
  185. package/dist/ghidra/GhidraInstallation.js +20 -15
  186. package/dist/ghidra/GhidraLauncher.js +93 -29
  187. package/dist/ghidra/GhidraProvider.js +61 -161
  188. package/dist/ghidra/GhidraProviderCapabilities.js +173 -0
  189. package/dist/ghidra/GhidraSessionValues.js +2 -0
  190. package/dist/ghidra/GhidraSocketConnection.js +44 -7
  191. package/dist/ghidra/GhidraTargetSnapshot.js +30 -0
  192. package/dist/ghidra/GhidraTransport.js +71 -0
  193. package/dist/identity.js +3 -2
  194. package/dist/main/messages.js +5 -0
  195. package/dist/main/reload.js +49 -0
  196. package/dist/main/shutdown.js +25 -0
  197. package/dist/main/startup.js +14 -0
  198. package/dist/main/state.js +26 -0
  199. package/dist/main/transport.js +61 -0
  200. package/dist/main/types.js +1 -0
  201. package/dist/main.js +35 -146
  202. package/dist/process/PrivateRuntimeRoot.js +3 -2
  203. package/dist/process/ProcessOwnership.js +35 -0
  204. package/dist/process/ProviderProcess.js +2 -0
  205. package/dist/replay/JavaScriptReplayWorker.js +64 -50
  206. package/dist/replay/LinuxJavaScriptReplayRunner.js +173 -390
  207. package/dist/replay/LinuxJavaScriptReplaySandbox.js +140 -0
  208. package/dist/replay/ReplayOutcome.js +95 -0
  209. package/dist/replay/ReplayProcessLifecycle.js +71 -0
  210. package/dist/server/ProcessCaptureElicitation.js +157 -0
  211. package/dist/server/createServer.js +119 -54
  212. package/dist/server/registerApplicationTools/characterization.js +57 -0
  213. package/dist/server/registerApplicationTools/compareVersions.js +34 -0
  214. package/dist/server/registerApplicationTools/controlledReplay.js +38 -0
  215. package/dist/server/registerApplicationTools/coverage.js +84 -0
  216. package/dist/server/registerApplicationTools/helpers.js +42 -0
  217. package/dist/server/registerApplicationTools/traceFeature.js +26 -0
  218. package/dist/server/registerApplicationTools/types.js +1 -0
  219. package/dist/server/registerApplicationTools.js +10 -112
  220. package/dist/server/registerBrowserTools.js +59 -83
  221. package/dist/server/registerElectronTools.js +31 -46
  222. package/dist/server/registerEnhancedTools.js +34 -42
  223. package/dist/server/registerEvidenceResources.js +54 -0
  224. package/dist/server/registerInvestigationTools/helpers.js +74 -0
  225. package/dist/server/registerInvestigationTools/types.js +1 -0
  226. package/dist/server/registerInvestigationTools.js +83 -116
  227. package/dist/server/registerManagedWorkflowTools/compareManagedMembers.js +65 -0
  228. package/dist/server/registerManagedWorkflowTools/contract.js +8 -0
  229. package/dist/server/registerManagedWorkflowTools/evidence.js +43 -0
  230. package/dist/server/registerManagedWorkflowTools/importManagedReconstruction.js +39 -0
  231. package/dist/server/registerManagedWorkflowTools/planManagedRuntimeCorrelation.js +39 -0
  232. package/dist/server/registerManagedWorkflowTools/projectManagedApplicationGraph.js +52 -0
  233. package/dist/server/registerManagedWorkflowTools/types.js +1 -0
  234. package/dist/server/registerManagedWorkflowTools/verifyManagedNativeBoundaries.js +75 -0
  235. package/dist/server/registerManagedWorkflowTools.js +10 -274
  236. package/dist/server/registerOfficialTools.js +73 -59
  237. package/dist/server/registerSessionRecordTools.js +137 -0
  238. package/dist/server/registerSessionStatusTool.js +2 -1
  239. package/dist/server/registerSessionTools.js +60 -164
  240. package/dist/server/sessionAvailabilityPolicy.js +10 -0
  241. package/dist/server/toolRegistrationOptions.js +101 -3
  242. package/dist/server/toolResult.js +5 -0
  243. package/package.json +10 -11
  244. package/skills/{rea-analysis → reverse-engineer-anything}/SKILL.md +20 -7
  245. package/scripts/prepare-node-pty.mjs +0 -35
  246. package/scripts/rebuild-native.mjs +0 -22
@@ -1,123 +1,27 @@
1
1
  import { constants } from "node:fs";
2
2
  import { access, readFile, readdir } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
- import { join } from "node:path";
4
+ import { delimiter, join } from "node:path";
5
5
  import { execFile } from "node:child_process";
6
6
  import { promisify } from "node:util";
7
7
  import { parseBinaryTarget } from "../domain/binaryTarget.js";
8
- import { supportsNodeVersion } from "../domain/runtimeVersion.js";
9
8
  import { probeHomebrew } from "./homebrew.js";
10
- import { linuxHopperBinarySupported, linuxHopperLauncherPath, linuxSharedLibrariesAvailable, readLinuxDistribution, } from "./LinuxHopper.js";
9
+ import { linuxHopperBinarySupported, linuxSharedLibrariesAvailable, readLinuxDistribution, } from "./LinuxHopper.js";
11
10
  import { CATALOG_IDENTITY } from "../catalogIdentity.js";
12
11
  import { PRODUCT_IDENTITY, SDK_IDENTITY } from "../identity.js";
13
12
  import { readClientRegistrationStatuses, } from "./ClientRegistrationStatus.js";
13
+ import { inspectRuntimeExecutables, } from "./RuntimeExecutableDiagnostics.js";
14
+ import { collectDoctorDiagnostics, doctorHealthy, } from "./DoctorDiagnostics.js";
14
15
  const execFileAsync = promisify(execFile);
15
- const DEFAULT_HOPPER = "/Applications/Hopper Disassembler.app/Contents/MacOS/hopper";
16
- const SYSTEM_LINUX_HOPPER = "/opt/hopper/bin/Hopper";
17
16
  /**
18
17
  * Check requirements and an optional target without mutating the host.
19
18
  * Every failed check includes remediation suitable for either the one-shot CLI
20
19
  * or an agent consuming the structured result.
21
20
  */
22
21
  export const runDoctor = async (target, host = systemDoctorHost()) => {
23
- const checks = [];
24
- checks.push(check("node", supportsNodeVersion(host.nodeVersion), host.nodeVersion, {
25
- remediation: "Install Node.js 22.19+ or 24.11+.",
26
- classification: "missing_dependency",
27
- }));
28
- const macosVersion = host.platform === "darwin" ? await host.macosVersion() : undefined;
29
- const macosMajor = macosVersion === undefined ? 0 : parseMajor(macosVersion);
30
- const linuxDistribution = host.platform === "linux" ? await host.linuxDistribution() : undefined;
31
- const supportedHost = (host.platform === "darwin" && macosMajor >= 12) ||
32
- (host.platform === "linux" && linuxDistribution?.supported === true);
33
- checks.push(check("host", supportedHost, macosVersion ??
34
- (linuxDistribution === undefined
35
- ? host.platform
36
- : `${linuxDistribution.id} ${linuxDistribution.versionId ?? "unknown"}`), {
37
- remediation: "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.",
38
- classification: "unsupported_host",
39
- }));
40
- const candidates = host.configuredHopperPath === undefined
41
- ? [
42
- DEFAULT_HOPPER,
43
- SYSTEM_LINUX_HOPPER,
44
- linuxHopperLauncherPath(homedir()),
45
- ...(await host.manualHopperPaths()),
46
- await host.brewHopperPath(),
47
- ].filter((value) => value !== undefined)
48
- : [host.configuredHopperPath];
49
- let hopperPath;
50
- for (const candidate of new Set(candidates))
51
- if (await host.executable(candidate)) {
52
- hopperPath = candidate;
53
- break;
54
- }
55
- checks.push(check("hopper", hopperPath !== undefined, hopperPath ?? host.configuredHopperPath, {
56
- remediation: host.configuredHopperPath === undefined
57
- ? "Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH."
58
- : "Unset or update HOPPER_LAUNCHER_PATH to an executable Hopper launcher.",
59
- classification: host.configuredHopperPath === undefined
60
- ? "missing_analysis_engine"
61
- : "config_drift",
62
- }));
63
- if (host.platform === "linux" && hopperPath !== undefined)
64
- checks.push(check("hopper-version", await host.supportedLinuxHopper(hopperPath), hopperPath, {
65
- remediation: unsupportedHopperRemediation(host.configuredHopperPath === hopperPath),
66
- classification: "config_drift",
67
- }));
68
- const providerInspections = await inspectDoctorProviders(host, checks);
69
- const javascriptReplayCheck = await host.javascriptReplayCheck?.();
70
- if (javascriptReplayCheck !== undefined)
71
- checks.push(javascriptReplayCheck);
72
- if (host.platform === "linux" && hopperPath !== undefined)
73
- checks.push(check("hopper-demo-runtime", await host.linuxDemoRuntimeReady(), undefined, {
74
- remediation: "Rerun rea setup to install the Xvfb, xauth, Python, X11, and XTEST packages required for Linux demo sessions.",
75
- classification: "missing_dependency",
76
- }));
77
- if (target !== undefined)
78
- checks.push(check("target", await host.validTarget(target), target, {
79
- remediation: "Supply a readable local app or program path.",
80
- classification: "config_drift",
81
- }));
82
- const installationPaths = (await host.installationPaths?.()) ?? [];
83
- const observedSkillIdentity = await host.installedSkillIdentity?.();
84
- const legacySkillVersion = observedSkillIdentity === undefined
85
- ? await host.installedSkillVersion?.()
86
- : undefined;
87
- const installedSkillIdentity = observedSkillIdentity ??
88
- (legacySkillVersion === undefined
89
- ? undefined
90
- : {
91
- version: legacySkillVersion,
92
- toolCount: null,
93
- catalogDigest: null,
94
- });
95
- const installedSkillVersion = installedSkillIdentity?.version ?? undefined;
96
- const skillAligned = installedSkillIdentity?.version === PRODUCT_IDENTITY.skillVersion &&
97
- installedSkillIdentity.toolCount === CATALOG_IDENTITY.counts.mcp_tools &&
98
- installedSkillIdentity.catalogDigest ===
99
- CATALOG_IDENTITY.digests.combined_sha256;
100
- if (!skillAligned)
101
- checks.push({
102
- name: "skill:identity",
103
- ok: false,
104
- classification: "config_drift",
105
- detail: installedSkillIdentity === undefined
106
- ? "Installed REA skill identity is missing."
107
- : "Installed REA skill identity is stale.",
108
- remediation: "Run rea setup to update the installed REA skill.",
109
- });
110
- const registrations = (await host.clientRegistrations?.()) ?? [];
111
- for (const registration of registrations)
112
- if (registration.state !== "aligned")
113
- checks.push({
114
- name: `registration:${registration.client}`,
115
- ok: false,
116
- classification: "config_drift",
117
- detail: registration.config_path,
118
- remediation: registration.remediation ??
119
- "Run rea setup, then restart the affected client.",
120
- });
22
+ const { checks: diagnosticChecks, hopperPath, providerInspections, runtimeExecutables, } = await collectDoctorDiagnostics(target, host);
23
+ const identity = await collectDoctorIdentity(host, runtimeExecutables);
24
+ const checks = [...diagnosticChecks, ...identity.checks];
121
25
  return {
122
26
  healthy: doctorHealthy(checks, {
123
27
  hopperAvailable: hopperPath !== undefined &&
@@ -130,7 +34,22 @@ export const runDoctor = async (target, host = systemDoctorHost()) => {
130
34
  ...(hopperPath === undefined ? {} : { hopperPath }),
131
35
  ...(providerInspections === undefined ? {} : { providerInspections }),
132
36
  checks,
133
- identity: {
37
+ identity: identity.value,
38
+ };
39
+ };
40
+ const collectDoctorIdentity = async (host, runtimeExecutables) => {
41
+ const installationPaths = (await host.installationPaths?.()) ?? [];
42
+ const installedSkillIdentity = await readInstalledSkillIdentity(host);
43
+ const skillAligned = skillIdentityAligned(installedSkillIdentity);
44
+ const registrations = (await host.clientRegistrations?.()) ?? [];
45
+ return {
46
+ checks: [
47
+ ...(skillAligned ? [] : [skillIdentityCheck(installedSkillIdentity)]),
48
+ ...registrations
49
+ .filter(({ state }) => state !== "aligned")
50
+ .map(registrationCheck),
51
+ ],
52
+ value: {
134
53
  cli_package_version: PRODUCT_IDENTITY.packageVersion,
135
54
  expected_skill_version: PRODUCT_IDENTITY.skillVersion,
136
55
  sdk: SDK_IDENTITY,
@@ -141,14 +60,10 @@ export const runDoctor = async (target, host = systemDoctorHost()) => {
141
60
  },
142
61
  installations: {
143
62
  paths: installationPaths,
144
- state: installationPaths.length === 0
145
- ? "unknown"
146
- : installationPaths.length === 1
147
- ? "single"
148
- : "multiple",
63
+ state: installationState(installationPaths),
149
64
  },
150
65
  skill: {
151
- installed_version: installedSkillVersion ?? null,
66
+ installed_version: installedSkillIdentity?.version ?? null,
152
67
  installed_tool_count: installedSkillIdentity?.toolCount ?? null,
153
68
  installed_catalog_digest: installedSkillIdentity?.catalogDigest ?? null,
154
69
  state: installedSkillIdentity === undefined
@@ -161,60 +76,58 @@ export const runDoctor = async (target, host = systemDoctorHost()) => {
161
76
  : "Run rea setup to update the installed REA skill.",
162
77
  },
163
78
  registrations,
79
+ runtime_executables: runtimeExecutables ?? null,
164
80
  },
165
81
  };
166
82
  };
167
- const unsupportedHopperRemediation = (configured) => configured
168
- ? "Unset or update HOPPER_LAUNCHER_PATH, install the Hopper build supported by this REA release, or update REA."
169
- : "Install the Hopper build supported by this REA release, or update REA for a newer Hopper build.";
83
+ const readInstalledSkillIdentity = async (host) => {
84
+ const observed = await host.installedSkillIdentity?.();
85
+ if (observed !== undefined)
86
+ return observed;
87
+ const legacyVersion = await host.installedSkillVersion?.();
88
+ return legacyVersion === undefined
89
+ ? undefined
90
+ : { version: legacyVersion, toolCount: null, catalogDigest: null };
91
+ };
92
+ const skillIdentityAligned = (identity) => identity?.version === PRODUCT_IDENTITY.skillVersion &&
93
+ identity.toolCount === CATALOG_IDENTITY.counts.mcp_tools &&
94
+ identity.catalogDigest === CATALOG_IDENTITY.digests.combined_sha256;
95
+ const skillIdentityCheck = (identity) => ({
96
+ name: "skill:identity",
97
+ ok: false,
98
+ classification: "config_drift",
99
+ detail: identity === undefined
100
+ ? "Installed REA skill identity is missing."
101
+ : "Installed REA skill identity is stale.",
102
+ remediation: "Run rea setup to update the installed REA skill.",
103
+ });
104
+ const registrationCheck = (registration) => ({
105
+ name: `registration:${registration.client}`,
106
+ ok: false,
107
+ classification: "config_drift",
108
+ detail: registration.config_path,
109
+ remediation: registration.remediation ??
110
+ "Run rea setup, then restart the affected client.",
111
+ });
112
+ const installationState = (paths) => paths.length === 0 ? "unknown" : paths.length === 1 ? "single" : "multiple";
170
113
  /** Create diagnostics backed by the current process and host commands. */
171
114
  export const systemDoctorHost = (options = {}) => ({
172
115
  platform: process.platform,
116
+ architecture: process.arch,
173
117
  nodeVersion: process.versions.node,
174
118
  ...(process.env.HOPPER_LAUNCHER_PATH === undefined
175
119
  ? {}
176
120
  : { configuredHopperPath: process.env.HOPPER_LAUNCHER_PATH }),
177
- async macosVersion() {
178
- try {
179
- return (await execFileAsync("sw_vers", ["-productVersion"])).stdout.trim();
180
- }
181
- catch {
182
- return undefined;
183
- }
184
- },
121
+ ...(process.env.REA_ILSPY_CMD_PATH === undefined
122
+ ? {}
123
+ : { configuredIlspyCmdPath: process.env.REA_ILSPY_CMD_PATH }),
124
+ macosVersion: readMacosVersion,
185
125
  linuxDistribution: readLinuxDistribution,
186
126
  async validTarget(path) {
187
127
  return (await parseBinaryTarget(path)).ok;
188
128
  },
189
- async executable(path) {
190
- try {
191
- await access(path, constants.X_OK);
192
- if (process.platform === "linux") {
193
- const linked = await execFileAsync("ldd", [path]);
194
- if (!linuxSharedLibrariesAvailable(`${linked.stdout}\n${linked.stderr}`))
195
- return false;
196
- }
197
- return true;
198
- }
199
- catch {
200
- return false;
201
- }
202
- },
203
- async linuxDemoRuntimeReady() {
204
- try {
205
- await access("/usr/bin/Xvfb", constants.X_OK);
206
- await access("/usr/bin/xauth", constants.X_OK);
207
- await access("/usr/bin/python3", constants.X_OK);
208
- await execFileAsync("/usr/bin/python3", [
209
- "-c",
210
- "import ctypes; ctypes.CDLL('libX11.so.6'); ctypes.CDLL('libXtst.so.6')",
211
- ]);
212
- return true;
213
- }
214
- catch {
215
- return false;
216
- }
217
- },
129
+ executable: executableAvailable,
130
+ linuxDemoRuntimeReady,
218
131
  supportedLinuxHopper: linuxHopperBinarySupported,
219
132
  async brewHopperPath() {
220
133
  return probeHomebrew(async (command) => {
@@ -231,117 +144,122 @@ export const systemDoctorHost = (options = {}) => ({
231
144
  }
232
145
  });
233
146
  },
234
- async manualHopperPaths() {
235
- const roots = ["/Applications", join(homedir(), "Applications")];
236
- const paths = [];
237
- for (const root of roots) {
238
- try {
239
- for (const entry of await readdir(root, { withFileTypes: true })) {
240
- if (entry.isDirectory() && /^Hopper.*\.app$/i.test(entry.name))
241
- paths.push(join(root, entry.name, "Contents/MacOS/hopper"));
242
- }
243
- }
244
- catch {
245
- // A missing or unreadable optional application directory is not fatal.
246
- }
247
- }
248
- return paths;
249
- },
147
+ manualHopperPaths,
250
148
  ...(options.providerInspections === undefined
251
149
  ? {}
252
150
  : { providerInspections: options.providerInspections }),
253
151
  ...(options.javascriptReplayCheck === undefined
254
152
  ? {}
255
153
  : { javascriptReplayCheck: options.javascriptReplayCheck }),
256
- async installationPaths() {
154
+ async ilspyCmdVersion(path) {
257
155
  try {
258
- const command = process.platform === "win32" ? "where" : "which";
259
- const arguments_ = process.platform === "win32" ? ["rea"] : ["-a", "rea"];
260
- return uniqueLines((await execFileAsync(command, arguments_)).stdout);
156
+ await access(path, constants.X_OK);
157
+ return firstIlspyVersionLine((await execFileAsync(path, ["--version"], { timeout: 10_000 })).stdout);
261
158
  }
262
159
  catch {
263
- return [];
160
+ return undefined;
264
161
  }
265
162
  },
266
- async installedSkillVersion() {
163
+ runtimeExecutables: () => inspectRuntimeExecutables({
164
+ platform: process.platform,
165
+ path: process.env.PATH ?? "",
166
+ launcherNode: process.execPath,
167
+ ...(process.env.PATHEXT === undefined
168
+ ? {}
169
+ : { pathExtensions: process.env.PATHEXT.split(delimiter) }),
170
+ }),
171
+ async installationPaths() {
267
172
  try {
268
- const content = await readFile(join(homedir(), ".agents/skills", PRODUCT_IDENTITY.skillName, "SKILL.md"), "utf8");
269
- return /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(content)?.[1];
173
+ const command = process.platform === "win32" ? "where" : "which";
174
+ const arguments_ = process.platform === "win32" ? ["rea"] : ["-a", "rea"];
175
+ return uniqueLines((await execFileAsync(command, arguments_)).stdout);
270
176
  }
271
177
  catch {
272
- return undefined;
178
+ return [];
273
179
  }
274
180
  },
275
- async installedSkillIdentity() {
181
+ installedSkillVersion,
182
+ installedSkillIdentity,
183
+ clientRegistrations: () => readClientRegistrationStatuses(homedir()),
184
+ });
185
+ const readMacosVersion = async () => {
186
+ try {
187
+ return (await execFileAsync("sw_vers", ["-productVersion"])).stdout.trim();
188
+ }
189
+ catch {
190
+ return undefined;
191
+ }
192
+ };
193
+ const executableAvailable = async (path) => {
194
+ try {
195
+ await access(path, constants.X_OK);
196
+ if (process.platform !== "linux")
197
+ return true;
198
+ const linked = await execFileAsync("ldd", [path]);
199
+ return linuxSharedLibrariesAvailable(`${linked.stdout}\n${linked.stderr}`);
200
+ }
201
+ catch {
202
+ return false;
203
+ }
204
+ };
205
+ const linuxDemoRuntimeReady = async () => {
206
+ try {
207
+ await access("/usr/bin/Xvfb", constants.X_OK);
208
+ await access("/usr/bin/xauth", constants.X_OK);
209
+ await access("/usr/bin/python3", constants.X_OK);
210
+ await execFileAsync("/usr/bin/python3", [
211
+ "-c",
212
+ "import ctypes; ctypes.CDLL('libX11.so.6'); ctypes.CDLL('libXtst.so.6')",
213
+ ]);
214
+ return true;
215
+ }
216
+ catch {
217
+ return false;
218
+ }
219
+ };
220
+ const manualHopperPaths = async () => {
221
+ const paths = [];
222
+ for (const root of ["/Applications", join(homedir(), "Applications")]) {
276
223
  try {
277
- const content = await readFile(join(homedir(), ".agents/skills", PRODUCT_IDENTITY.skillName, "SKILL.md"), "utf8");
278
- const version = /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(content)?.[1] ?? null;
279
- const countText = /^\s{2}tool_count:\s*(\d+)\s*$/mu.exec(content)?.[1];
280
- const catalogDigest = /^\s{2}catalog_digest:\s*"([a-f0-9]{64})"\s*$/mu.exec(content)?.[1] ??
281
- null;
282
- return {
283
- version,
284
- toolCount: countText === undefined ? null : Number.parseInt(countText, 10),
285
- catalogDigest,
286
- };
224
+ for (const entry of await readdir(root, { withFileTypes: true })) {
225
+ if (entry.isDirectory() && /^Hopper.*\.app$/i.test(entry.name))
226
+ paths.push(join(root, entry.name, "Contents/MacOS/hopper"));
227
+ }
287
228
  }
288
229
  catch {
289
- return undefined;
230
+ // A missing or unreadable optional application directory is not fatal.
290
231
  }
291
- },
292
- clientRegistrations: () => readClientRegistrationStatuses(homedir()),
293
- });
294
- const parseMajor = (version) => Number.parseInt(version.split(".")[0] ?? "0", 10);
232
+ }
233
+ return paths;
234
+ };
235
+ const readInstalledSkill = () => readFile(join(homedir(), ".agents/skills", PRODUCT_IDENTITY.skillName, "SKILL.md"), "utf8");
236
+ const installedSkillVersion = async () => {
237
+ try {
238
+ return /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(await readInstalledSkill())?.[1];
239
+ }
240
+ catch {
241
+ return undefined;
242
+ }
243
+ };
244
+ const installedSkillIdentity = async () => {
245
+ try {
246
+ const content = await readInstalledSkill();
247
+ const countText = /^\s{2}tool_count:\s*(\d+)\s*$/mu.exec(content)?.[1];
248
+ return {
249
+ version: /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(content)?.[1] ?? null,
250
+ toolCount: countText === undefined ? null : Number.parseInt(countText, 10),
251
+ catalogDigest: /^\s{2}catalog_digest:\s*"([a-f0-9]{64})"\s*$/mu.exec(content)?.[1] ??
252
+ null,
253
+ };
254
+ }
255
+ catch {
256
+ return undefined;
257
+ }
258
+ };
295
259
  const uniqueLines = (value) => [
296
260
  ...new Set(value
297
261
  .split(/\r?\n/u)
298
262
  .map((line) => line.trim())
299
263
  .filter((line) => line.length > 0)),
300
264
  ];
301
- const check = (name, ok, detail, failure) => ({
302
- name,
303
- ok,
304
- classification: ok ? "healthy" : failure.classification,
305
- ...(detail === undefined ? {} : { detail }),
306
- ...(ok ? {} : { remediation: failure.remediation }),
307
- });
308
- const providerDoctorChecks = (inspection) => inspection.checks.map((candidate) => ({
309
- name: candidate.name === "configuration"
310
- ? inspection.id
311
- : `${inspection.id}-${candidate.name}`,
312
- ok: candidate.ok,
313
- classification: candidate.ok ? "healthy" : candidate.classification,
314
- detail: candidate.detail,
315
- ...(candidate.remediation === null
316
- ? {}
317
- : { remediation: candidate.remediation }),
318
- }));
319
- const inspectDoctorProviders = async (host, checks) => {
320
- const inspections = await host.providerInspections?.();
321
- if (inspections === undefined)
322
- return undefined;
323
- for (const inspection of inspections)
324
- checks.push(...providerDoctorChecks(inspection));
325
- return inspections;
326
- };
327
- const doctorHealthy = (checks, providers) => {
328
- if (providers.providerInspections === undefined)
329
- return checks.every(({ ok }) => ok);
330
- const providerChecks = checks.filter(({ name }) => name.startsWith("hopper") ||
331
- providers.providerInspections?.some((inspection) => providerCheckName(inspection.id, name)) === true);
332
- const coreHealthy = checks
333
- .filter((candidate) => !providerChecks.includes(candidate))
334
- .every(({ ok }) => ok);
335
- const configuredProvidersHealthy = providerChecks
336
- .filter(({ name }) => {
337
- if (name.startsWith("hopper"))
338
- return providers.hopperConfigured;
339
- return providers.providerInspections?.some((inspection) => inspection.configured && providerCheckName(inspection.id, name));
340
- })
341
- .every(({ ok }) => ok);
342
- return (coreHealthy &&
343
- configuredProvidersHealthy &&
344
- (providers.hopperAvailable ||
345
- providers.providerInspections.some(({ available }) => available)));
346
- };
347
- const providerCheckName = (providerId, name) => name === providerId || name.startsWith(`${providerId}-`);
265
+ const firstIlspyVersionLine = (value) => uniqueLines(value).find((line) => line.startsWith("ilspycmd:"));
@@ -0,0 +1,180 @@
1
+ import { homedir } from "node:os";
2
+ import { supportsNodeVersion } from "../domain/runtimeVersion.js";
3
+ import { linuxHopperLauncherPath } from "./LinuxHopper.js";
4
+ const DEFAULT_HOPPER = "/Applications/Hopper Disassembler.app/Contents/MacOS/hopper";
5
+ const SYSTEM_LINUX_HOPPER = "/opt/hopper/bin/Hopper";
6
+ /** Collect host, provider, and optional-target diagnostics without mutation. */
7
+ export const collectDoctorDiagnostics = async (target, host) => {
8
+ const runtimeExecutables = await host.runtimeExecutables?.();
9
+ const checks = [
10
+ nodeCheck(host),
11
+ ...runtimeChecks(runtimeExecutables),
12
+ ];
13
+ checks.push(await hostCheck(host));
14
+ const hopperPath = await findHopper(host);
15
+ checks.push(hopperCheck(host, hopperPath));
16
+ const hopperVersionCheck = await optionalHopperVersionCheck(host, hopperPath);
17
+ if (hopperVersionCheck !== undefined)
18
+ checks.push(hopperVersionCheck);
19
+ const providerInspections = await host.providerInspections?.();
20
+ if (providerInspections !== undefined)
21
+ checks.push(...providerInspections.flatMap(providerDoctorChecks));
22
+ const javascriptReplayCheck = await host.javascriptReplayCheck?.();
23
+ if (javascriptReplayCheck !== undefined)
24
+ checks.push(javascriptReplayCheck);
25
+ const ilspyCheck = await optionalIlspyCheck(host);
26
+ if (ilspyCheck !== undefined)
27
+ checks.push(ilspyCheck);
28
+ const hopperDemoCheck = await optionalHopperDemoCheck(host, hopperPath);
29
+ if (hopperDemoCheck !== undefined)
30
+ checks.push(hopperDemoCheck);
31
+ const targetCheck = await optionalTargetCheck(target, host);
32
+ if (targetCheck !== undefined)
33
+ checks.push(targetCheck);
34
+ return {
35
+ checks,
36
+ ...(hopperPath === undefined ? {} : { hopperPath }),
37
+ ...(providerInspections === undefined ? {} : { providerInspections }),
38
+ ...(runtimeExecutables === undefined ? {} : { runtimeExecutables }),
39
+ };
40
+ };
41
+ /** Evaluate core health while treating unconfigured providers as optional. */
42
+ export const doctorHealthy = (checks, providers) => {
43
+ if (providers.providerInspections === undefined)
44
+ return checks.every(({ ok }) => ok);
45
+ const providerChecks = checks.filter(({ name }) => name.startsWith("hopper") ||
46
+ providers.providerInspections?.some((inspection) => providerCheckName(inspection.id, name)) === true);
47
+ const coreHealthy = checks
48
+ .filter((candidate) => !providerChecks.includes(candidate))
49
+ .every(({ ok }) => ok);
50
+ const configuredProvidersHealthy = providerChecks
51
+ .filter(({ name }) => {
52
+ if (name.startsWith("hopper"))
53
+ return providers.hopperConfigured;
54
+ return providers.providerInspections?.some((inspection) => inspection.configured && providerCheckName(inspection.id, name));
55
+ })
56
+ .every(({ ok }) => ok);
57
+ return (coreHealthy &&
58
+ configuredProvidersHealthy &&
59
+ (providers.hopperAvailable ||
60
+ providers.providerInspections.some(({ available }) => available)));
61
+ };
62
+ const nodeCheck = (host) => check("node", supportsNodeVersion(host.nodeVersion), host.nodeVersion, {
63
+ remediation: "Install Node.js 22.19+ or 24.11+.",
64
+ classification: "missing_dependency",
65
+ });
66
+ const runtimeChecks = (inventory) => {
67
+ if (inventory === undefined)
68
+ return [];
69
+ const broken = inventory.candidates.filter(({ healthy }) => !healthy);
70
+ const detail = broken.length === 0
71
+ ? `${String(inventory.candidates.length)} runtime executable candidates passed bounded version probes.`
72
+ : `${String(broken.length)} of ${String(inventory.candidates.length)} runtime executable candidates failed bounded version probes: ${broken
73
+ .slice(0, 5)
74
+ .map(({ lexical_path: path }) => path)
75
+ .join(", ")}${broken.length > 5 ? ", …" : ""}`;
76
+ return [
77
+ check("node-toolchains", broken.length === 0, detail, {
78
+ remediation: "Select a verified healthy Node toolchain or repair the failing installation, then rerun rea doctor. Do not create compatibility-library symlinks.",
79
+ classification: "missing_dependency",
80
+ }),
81
+ ];
82
+ };
83
+ const hostCheck = async (host) => {
84
+ const macosVersion = host.platform === "darwin" ? await host.macosVersion() : undefined;
85
+ const linuxDistribution = host.platform === "linux" ? await host.linuxDistribution() : undefined;
86
+ const supported = (host.platform === "darwin" && parseMajor(macosVersion) >= 12) ||
87
+ (host.platform === "linux" && linuxDistribution?.supported === true) ||
88
+ (host.platform === "win32" && host.architecture === "x64");
89
+ const detail = macosVersion ??
90
+ (linuxDistribution === undefined
91
+ ? `${host.platform} ${host.architecture}`
92
+ : `${linuxDistribution.id} ${linuxDistribution.versionId ?? "unknown"}`);
93
+ return check("host", supported, detail, {
94
+ remediation: "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and the experimental Windows x64 Ghidra P0 boundary.",
95
+ classification: "unsupported_host",
96
+ });
97
+ };
98
+ const findHopper = async (host) => {
99
+ const candidates = host.configuredHopperPath === undefined
100
+ ? [
101
+ DEFAULT_HOPPER,
102
+ SYSTEM_LINUX_HOPPER,
103
+ linuxHopperLauncherPath(homedir()),
104
+ ...(await host.manualHopperPaths()),
105
+ ]
106
+ : [host.configuredHopperPath];
107
+ const hopperPath = await firstExecutable(host, candidates);
108
+ if (hopperPath !== undefined || host.configuredHopperPath !== undefined)
109
+ return hopperPath;
110
+ const brewCandidate = await host.brewHopperPath();
111
+ return firstExecutable(host, brewCandidate === undefined ? [] : [brewCandidate]);
112
+ };
113
+ const firstExecutable = async (host, candidates) => {
114
+ for (const candidate of new Set(candidates))
115
+ if (await host.executable(candidate))
116
+ return candidate;
117
+ return undefined;
118
+ };
119
+ const hopperCheck = (host, hopperPath) => check("hopper", hopperPath !== undefined, hopperPath ?? host.configuredHopperPath, {
120
+ remediation: host.configuredHopperPath === undefined
121
+ ? "Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH."
122
+ : "Unset or update HOPPER_LAUNCHER_PATH to an executable Hopper launcher.",
123
+ classification: host.configuredHopperPath === undefined
124
+ ? "missing_analysis_engine"
125
+ : "config_drift",
126
+ });
127
+ const optionalHopperVersionCheck = async (host, hopperPath) => {
128
+ if (host.platform !== "linux" || hopperPath === undefined)
129
+ return undefined;
130
+ return check("hopper-version", await host.supportedLinuxHopper(hopperPath), hopperPath, {
131
+ remediation: unsupportedHopperRemediation(host.configuredHopperPath === hopperPath),
132
+ classification: "config_drift",
133
+ });
134
+ };
135
+ const optionalHopperDemoCheck = async (host, hopperPath) => host.platform !== "linux" || hopperPath === undefined
136
+ ? undefined
137
+ : check("hopper-demo-runtime", await host.linuxDemoRuntimeReady(), undefined, {
138
+ remediation: "Rerun rea setup to install the Xvfb, xauth, Python, X11, and XTEST packages required for Linux demo sessions.",
139
+ classification: "missing_dependency",
140
+ });
141
+ const optionalIlspyCheck = async (host) => {
142
+ if (host.configuredIlspyCmdPath === undefined)
143
+ return undefined;
144
+ const version = await host.ilspyCmdVersion?.(host.configuredIlspyCmdPath);
145
+ return check("ilspycmd", version !== undefined, version === undefined
146
+ ? host.configuredIlspyCmdPath
147
+ : `${host.configuredIlspyCmdPath} (${version})`, {
148
+ remediation: "Unset REA_ILSPY_CMD_PATH or point it at a runnable ilspycmd executable.",
149
+ classification: "config_drift",
150
+ });
151
+ };
152
+ const optionalTargetCheck = async (target, host) => target === undefined
153
+ ? undefined
154
+ : check("target", await host.validTarget(target), target, {
155
+ remediation: "Supply a readable local app or program path.",
156
+ classification: "config_drift",
157
+ });
158
+ const check = (name, ok, detail, failure) => ({
159
+ name,
160
+ ok,
161
+ classification: ok ? "healthy" : failure.classification,
162
+ ...(detail === undefined ? {} : { detail }),
163
+ ...(ok ? {} : { remediation: failure.remediation }),
164
+ });
165
+ const providerDoctorChecks = (inspection) => inspection.checks.map((candidate) => ({
166
+ name: candidate.name === "configuration"
167
+ ? inspection.id
168
+ : `${inspection.id}-${candidate.name}`,
169
+ ok: candidate.ok,
170
+ classification: candidate.ok ? "healthy" : candidate.classification,
171
+ detail: candidate.detail,
172
+ ...(candidate.remediation === null
173
+ ? {}
174
+ : { remediation: candidate.remediation }),
175
+ }));
176
+ const unsupportedHopperRemediation = (configured) => configured
177
+ ? "Unset or update HOPPER_LAUNCHER_PATH, install the Hopper build supported by this REA release, or update REA."
178
+ : "Install the Hopper build supported by this REA release, or update REA for a newer Hopper build.";
179
+ const parseMajor = (version) => Number.parseInt(version?.split(".")[0] ?? "0", 10);
180
+ const providerCheckName = (providerId, name) => name === providerId || name.startsWith(`${providerId}-`);