rea-agents 1.2.0 → 1.4.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 (107) hide show
  1. package/README.md +95 -27
  2. package/bridge/hopper_bridge.py +172 -14
  3. package/dist/application/AnalysisSnapshotCache.js +131 -0
  4. package/dist/application/AnalysisSnapshotFiles.js +31 -0
  5. package/dist/application/ArtifactGraphConstruction.js +1 -1
  6. package/dist/application/ArtifactInventory.js +100 -24
  7. package/dist/application/AuthorizedArtifactInventory.js +16 -0
  8. package/dist/application/BinarySession.js +149 -36
  9. package/dist/application/BinarySessionPort.js +1 -0
  10. package/dist/application/BoundedJsonFiles.js +109 -0
  11. package/dist/application/CapabilityInventory.js +127 -0
  12. package/dist/application/ClientRegistrationStatus.js +69 -0
  13. package/dist/application/CrossVersionInventory.js +109 -0
  14. package/dist/application/CrossVersionInvestigation.js +358 -0
  15. package/dist/application/DirectAnalysis.js +205 -24
  16. package/dist/application/Doctor.js +104 -3
  17. package/dist/application/EvidenceBundleFiles.js +15 -108
  18. package/dist/application/EvidenceLedger.js +3 -2
  19. package/dist/application/InvestigationProviders.js +48 -0
  20. package/dist/application/InvestigationWorkspaceStore.js +214 -0
  21. package/dist/application/LinuxHopper.js +72 -48
  22. package/dist/application/PermissionAuthority.js +183 -0
  23. package/dist/application/PermissionConfiguration.js +26 -0
  24. package/dist/application/ProcessCaptureAuthority.js +2 -2
  25. package/dist/application/ProcessCaptureError.js +25 -0
  26. package/dist/application/ProcessCaptureLifecycle.js +22 -2
  27. package/dist/application/ProcessCli.js +111 -31
  28. package/dist/application/ProcessHarness.js +6 -3
  29. package/dist/application/ProgressReporter.js +30 -0
  30. package/dist/application/ProjectPermissionStore.js +110 -0
  31. package/dist/application/ReferenceSourceImportEntries.js +18 -3
  32. package/dist/application/ReferenceSourceImportTypes.js +32 -0
  33. package/dist/application/Setup.js +35 -101
  34. package/dist/application/SetupClients.js +19 -0
  35. package/dist/application/SetupInstallFailure.js +53 -0
  36. package/dist/application/SetupPlan.js +31 -0
  37. package/dist/application/SupportedClients.js +41 -0
  38. package/dist/application/Uninstall.js +34 -24
  39. package/dist/application/UnknownEvidence.js +33 -0
  40. package/dist/application/Upgrade.js +3 -1
  41. package/dist/application/runtime.js +2 -2
  42. package/dist/artifacts/ArtifactProvider.js +15 -8
  43. package/dist/catalogIdentity.js +121 -0
  44. package/dist/cli.js +76 -13
  45. package/dist/cliEvidenceCommands.js +52 -12
  46. package/dist/cliInvestigationCommands.js +137 -0
  47. package/dist/cliLogging.js +20 -3
  48. package/dist/cliOutput.js +41 -0
  49. package/dist/cliPolicyCommands.js +115 -0
  50. package/dist/config.js +102 -27
  51. package/dist/contracts/artifactToolContracts.js +14 -1
  52. package/dist/contracts/errorSchemas.js +98 -0
  53. package/dist/contracts/promptContracts.js +256 -0
  54. package/dist/contracts/sessionLifecycleInputs.js +11 -0
  55. package/dist/contracts/toolContractTypes.js +1 -0
  56. package/dist/contracts/toolContracts.js +20 -7
  57. package/dist/contracts/toolOutputSchemas.js +70 -9
  58. package/dist/domain/analysisSnapshot.js +149 -0
  59. package/dist/domain/artifactComparison.js +38 -11
  60. package/dist/domain/artifactGraph.js +25 -1
  61. package/dist/domain/artifactInventoryEvidence.js +18 -0
  62. package/dist/domain/changedBehavior.js +35 -10
  63. package/dist/domain/errors.js +384 -32
  64. package/dist/domain/evidence.js +4 -2
  65. package/dist/domain/evidenceBundle.js +28 -0
  66. package/dist/domain/hopperStartupFailure.js +55 -0
  67. package/dist/domain/investigationWorkspace.js +341 -0
  68. package/dist/domain/jsonValue.js +2 -0
  69. package/dist/domain/nativeInspection.js +3 -2
  70. package/dist/domain/permissionPolicy.js +157 -0
  71. package/dist/domain/processCapture.js +5 -4
  72. package/dist/domain/processComparison.js +6 -4
  73. package/dist/domain/reconstructionVerification.js +1 -1
  74. package/dist/domain/reconstructionVerificationSchemas.js +1 -0
  75. package/dist/domain/staticRuntimeCorrelation.js +5 -1
  76. package/dist/generatedPackageMetadata.js +9 -0
  77. package/dist/hopper/BridgeLauncher.js +51 -20
  78. package/dist/hopper/HopperClient.js +45 -3
  79. package/dist/hopper/HopperProvider.js +1 -0
  80. package/dist/identity.js +10 -3
  81. package/dist/logger.js +2 -2
  82. package/dist/main.js +139 -28
  83. package/dist/server/createServer.js +54 -5
  84. package/dist/server/mcpProgress.js +23 -0
  85. package/dist/server/promptCompletion.js +144 -0
  86. package/dist/server/registerArtifactComparisonTool.js +6 -2
  87. package/dist/server/registerBundleComparisonTool.js +6 -2
  88. package/dist/server/registerEnhancedTools.js +20 -7
  89. package/dist/server/registerEvidenceResources.js +302 -0
  90. package/dist/server/registerEvidenceTools.js +59 -8
  91. package/dist/server/registerFunctionComparisonTool.js +7 -3
  92. package/dist/server/registerInvestigationTools.js +111 -18
  93. package/dist/server/registerOfficialTools.js +25 -14
  94. package/dist/server/registerProcessComparisonTool.js +8 -10
  95. package/dist/server/registerPrompts.js +67 -0
  96. package/dist/server/registerSessionStatusTool.js +52 -0
  97. package/dist/server/registerSessionTools.js +178 -22
  98. package/dist/server/runDerivedOperation.js +35 -0
  99. package/dist/server/sessionEvidence.js +2 -8
  100. package/dist/server/sessionToolPolicies.js +1 -42
  101. package/dist/server/toolResult.js +34 -6
  102. package/dist/serverIdentity.js +70 -0
  103. package/install.sh +11 -11
  104. package/package.json +10 -6
  105. package/scripts/hopper-demo-x11.py +349 -0
  106. package/scripts/rea.mjs +6 -3
  107. package/skills/rea-analysis/SKILL.md +9 -2
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node-pty";
2
2
  import { validateProcessCapture } from "../domain/processCapture.js";
3
3
  import { err, ok } from "../domain/result.js";
4
- import { ProcessCaptureError } from "./ProcessCaptureError.js";
4
+ import { ProcessCaptureError, processCaptureCancelled, } from "./ProcessCaptureError.js";
5
5
  export { ProcessCaptureError } from "./ProcessCaptureError.js";
6
6
  import { startLoopbackReplay } from "./LoopbackReplay.js";
7
7
  import { startProcessSampler } from "./ProcessSampling.js";
@@ -98,7 +98,10 @@ const cleanupFailedStartup = async (options) => {
98
98
  sampledProcessGroupIds: [],
99
99
  });
100
100
  if (cleanupFailure !== undefined)
101
- throw new ProcessCaptureError(cleanupFailure, { cause: options.cause });
101
+ throw new ProcessCaptureError(cleanupFailure, {
102
+ cause: options.cause,
103
+ reason: "cleanup_incomplete",
104
+ });
102
105
  throw options.cause;
103
106
  };
104
107
  const createTerminalRenderer = (scenario, temporaryRoot, terminalPid) => new TerminalRenderer({
@@ -218,7 +221,7 @@ const completeCapture = async (options) => {
218
221
  runtime.checkpoints.trigger("root_exit");
219
222
  const { reason } = options.exit;
220
223
  if (reason === "cancelled")
221
- throw new ProcessCaptureError("process capture was cancelled");
224
+ throw processCaptureCancelled();
222
225
  const settlement = await observeSettlement(options.runId, [
223
226
  runtime.terminal.pid,
224
227
  ...options.samples.flatMap(({ process_group_id }) => process_group_id === null ? [] : [process_group_id]),
@@ -0,0 +1,30 @@
1
+ /** Create a monotonic, rate-bounded reporter around a surface-specific sink. */
2
+ export const createProgressReporter = (sink, options = {}) => {
3
+ const minimumIntervalMs = options.minimumIntervalMs ?? 100;
4
+ const now = options.now ?? Date.now;
5
+ let lastCompleted = -1;
6
+ let lastSentAt = Number.NEGATIVE_INFINITY;
7
+ let sequence = 0;
8
+ return {
9
+ async report(update) {
10
+ if (!Number.isFinite(update.completed) || update.completed < 0)
11
+ throw new TypeError("Progress completed must be a nonnegative number");
12
+ if (update.total !== null && update.total < update.completed)
13
+ throw new TypeError("Progress total cannot be less than completed");
14
+ if (update.completed < lastCompleted)
15
+ throw new TypeError("Progress cannot move backwards");
16
+ lastCompleted = update.completed;
17
+ const observedAt = now();
18
+ if (update.terminal !== true &&
19
+ observedAt - lastSentAt < minimumIntervalMs)
20
+ return;
21
+ lastSentAt = observedAt;
22
+ sequence += 1;
23
+ await sink({ ...update, sequence });
24
+ },
25
+ };
26
+ };
27
+ /** Surface that deliberately ignores progress while preserving one workflow API. */
28
+ export const silentProgressReporter = {
29
+ report: () => Promise.resolve(),
30
+ };
@@ -0,0 +1,110 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, realpath, stat } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import writeFileAtomic from "write-file-atomic";
5
+ import { z } from "zod";
6
+ import { err, ok } from "../domain/result.js";
7
+ const grantSchema = z.object({
8
+ grant_id: z.string().min(1),
9
+ capability: z.enum([
10
+ "process_capture",
11
+ "evidence_read",
12
+ "evidence_write",
13
+ "investigation_input",
14
+ "investigation_workspace_read",
15
+ "investigation_workspace_write",
16
+ "snapshot_read",
17
+ "snapshot_write",
18
+ "artifact_extract",
19
+ "native_mount",
20
+ "reference_read",
21
+ ]),
22
+ roots: z.array(z.string()),
23
+ executables: z.array(z.string()),
24
+ environment_names: z.array(z.string()),
25
+ network: z.enum(["none", "loopback", "external"]),
26
+ mount: z.boolean(),
27
+ lifetime: z.literal("project"),
28
+ operation_identity: z.string().nullable(),
29
+ expires_at: z.iso.datetime().nullable(),
30
+ });
31
+ const storeSchema = z.object({
32
+ schema_version: z.literal(1),
33
+ project_id: z.string().regex(/^project_[a-f0-9]{64}$/u),
34
+ project_root: z.string(),
35
+ grants: z.array(grantSchema).max(1_000),
36
+ });
37
+ /** Owner-only project policy persistence failure. */
38
+ export class ProjectPermissionStoreError extends Error {
39
+ reason;
40
+ _tag = "ProjectPermissionStoreError";
41
+ constructor(reason, options) {
42
+ super(`Project permission store failed: ${reason}`, options);
43
+ this.reason = reason;
44
+ }
45
+ }
46
+ /** Derive relocation-explicit project identity from its canonical root. */
47
+ const identifyPermissionProject = async (projectRoot) => {
48
+ try {
49
+ const root = await realpath(projectRoot);
50
+ return ok({
51
+ id: `project_${createHash("sha256").update(root).digest("hex")}`,
52
+ root,
53
+ });
54
+ }
55
+ catch (cause) {
56
+ return err(new ProjectPermissionStoreError("project_not_found", { cause }));
57
+ }
58
+ };
59
+ /** Read and validate an owner-only store bound to the requested project. */
60
+ export const readProjectPermissionStore = async (path, projectRoot) => {
61
+ const project = await identifyPermissionProject(projectRoot);
62
+ if (!project.ok)
63
+ return project;
64
+ try {
65
+ const metadata = await stat(path);
66
+ if ((metadata.mode & 0o077) !== 0)
67
+ return err(new ProjectPermissionStoreError("not_owner_only"));
68
+ const parsed = storeSchema.safeParse(JSON.parse(await readFile(path, "utf8")));
69
+ if (!parsed.success ||
70
+ parsed.data.project_id !== project.value.id ||
71
+ parsed.data.project_root !== project.value.root)
72
+ return err(new ProjectPermissionStoreError("invalid"));
73
+ return ok(parsed.data);
74
+ }
75
+ catch (cause) {
76
+ if (isNotFound(cause))
77
+ return ok(null);
78
+ return err(new ProjectPermissionStoreError("io", { cause }));
79
+ }
80
+ };
81
+ /** Atomically replace explicit project grants with owner-only permissions. */
82
+ export const writeProjectPermissionStore = async (path, projectRoot, grants) => {
83
+ const project = await identifyPermissionProject(projectRoot);
84
+ if (!project.ok)
85
+ return project;
86
+ const candidate = storeSchema.safeParse({
87
+ schema_version: 1,
88
+ project_id: project.value.id,
89
+ project_root: project.value.root,
90
+ grants,
91
+ });
92
+ if (!candidate.success)
93
+ return err(new ProjectPermissionStoreError("invalid", { cause: candidate.error }));
94
+ try {
95
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
96
+ await writeFileAtomic(path, `${JSON.stringify(candidate.data, null, 2)}\n`, {
97
+ encoding: "utf8",
98
+ mode: 0o600,
99
+ });
100
+ const metadata = await stat(path);
101
+ if ((metadata.mode & 0o077) !== 0) {
102
+ return err(new ProjectPermissionStoreError("not_owner_only"));
103
+ }
104
+ return ok(candidate.data);
105
+ }
106
+ catch (cause) {
107
+ return err(new ProjectPermissionStoreError("io", { cause }));
108
+ }
109
+ };
110
+ const isNotFound = (cause) => cause instanceof Error && "code" in cause && cause.code === "ENOENT";
@@ -3,16 +3,31 @@ import { posix } from "node:path";
3
3
  import { classifyReferenceSourcePath, detectReferenceSourceLanguage, } from "../domain/referenceSourceClassification.js";
4
4
  import { parseReferenceSourceImports } from "../domain/referenceSourceImportParsing.js";
5
5
  import { PARSEABLE_REFERENCE_SOURCE_LANGUAGES } from "./ReferenceSourceImportTypes.js";
6
+ /** Project a low-level entry failure into safe import guidance. */
7
+ export const projectReferenceSourceEntryFailure = (entry) => {
8
+ if (entry.code === "cancelled")
9
+ return "This entry was not read because the import was cancelled. Start the import again when ready.";
10
+ if (entry.code === "limit")
11
+ return "This entry was not read because an import limit was reached. Import a smaller directory or raise the configured limits.";
12
+ if (entry.code === "unsupported")
13
+ return "This entry cannot be read safely on this system. Exclude it or import the directory on a supported system.";
14
+ if (entry.kind === "directory")
15
+ return "This directory could not be read. Check its permissions, then try again.";
16
+ if (entry.kind === "symlink")
17
+ return "This symbolic link could not be read safely. Check the link and its permissions, then try again.";
18
+ return "This file could not be read. Check its permissions, then try again.";
19
+ };
6
20
  const hashBytes = (bytes) => createHash("sha256").update(bytes).digest("hex");
7
21
  const failedEntry = (entry) => {
8
22
  const classifications = classifyReferenceSourcePath(entry.path);
23
+ const limitation = projectReferenceSourceEntryFailure(entry);
9
24
  if (entry.kind === "directory")
10
25
  return {
11
26
  path: entry.path,
12
27
  kind: "directory",
13
28
  classifications,
14
29
  tree_state: entry.code === "limit" ? "partial" : "unreadable",
15
- limitations: [entry.message],
30
+ limitations: [limitation],
16
31
  };
17
32
  if (entry.kind === "symlink")
18
33
  return {
@@ -21,7 +36,7 @@ const failedEntry = (entry) => {
21
36
  target: "<unreadable>",
22
37
  target_state: "unreadable",
23
38
  classifications,
24
- limitations: [entry.message],
39
+ limitations: [limitation],
25
40
  };
26
41
  return {
27
42
  path: entry.path,
@@ -31,7 +46,7 @@ const failedEntry = (entry) => {
31
46
  language: null,
32
47
  classifications: entry.kind === "file" ? classifications : ["unknown"],
33
48
  content_state: entry.code === "limit" ? "too-large" : "unreadable",
34
- limitations: [entry.message],
49
+ limitations: [limitation],
35
50
  };
36
51
  };
37
52
  const resolveInternalSpecifier = (fromPath, specifier, filePaths) => {
@@ -1,3 +1,35 @@
1
+ /** Safe CLI projection for a historical-source import failure. */
2
+ export const projectReferenceSourceImportError = (error) => {
3
+ if (error.code === "cancelled")
4
+ return {
5
+ category: "cancelled",
6
+ message: "Reference source import was cancelled. Start it again when ready.",
7
+ };
8
+ if (error.code === "invalid-limits")
9
+ return {
10
+ category: "invalid_input",
11
+ message: "Reference source limits are invalid. Use positive integer limits, then try again.",
12
+ };
13
+ if (error.code === "invalid-root")
14
+ return {
15
+ category: "invalid_input",
16
+ message: "Reference source directory could not be opened. Check that the path exists, is readable, and points to a directory.",
17
+ };
18
+ if (error.code === "policy")
19
+ return {
20
+ category: "permission_required",
21
+ message: "Reference source directory is not approved. Add its directory to `REA_REFERENCE_ROOTS_JSON`, restart REA, then try again.",
22
+ };
23
+ if (error.code === "io")
24
+ return {
25
+ category: "execution_failure",
26
+ message: "Reference source files could not be read. Check directory permissions and try again.",
27
+ };
28
+ return {
29
+ category: "execution_failure",
30
+ message: "Reference source could not be indexed. Check that the source tree is readable, then try again.",
31
+ };
32
+ };
1
33
  export const DEFAULT_REFERENCE_SOURCE_IGNORE_PATTERNS = [
2
34
  ".git/",
3
35
  ".git/hooks/",
@@ -1,6 +1,6 @@
1
1
  import { access, copyFile, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
- import { dirname, join, resolve } from "node:path";
3
+ import { dirname, resolve } from "node:path";
4
4
  import { z } from "zod";
5
5
  import writeFileAtomic from "write-file-atomic";
6
6
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
@@ -9,50 +9,15 @@ import { supportsNodeVersion } from "../domain/runtimeVersion.js";
9
9
  import { runDoctor, systemDoctorHost } from "./Doctor.js";
10
10
  import { installLinuxHopper, readLinuxDistribution, } from "./LinuxHopper.js";
11
11
  import { installMacHopper } from "./MacHopper.js";
12
+ import { configureDetectedClients } from "./SetupClients.js";
13
+ import { supportedClients } from "./SupportedClients.js";
12
14
  import { installCanonicalSkill } from "./SetupSkill.js";
15
+ import { setupPlan } from "./SetupPlan.js";
16
+ import { setupInstallFailure, } from "./SetupInstallFailure.js";
13
17
  export { installCanonicalSkill } from "./SetupSkill.js";
14
18
  const registrationCommand = () => process.env.npm_command === "exec"
15
19
  ? ["npx", "-y", PRODUCT_IDENTITY.packageName, "mcp"]
16
20
  : [resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), "mcp"];
17
- const setupPlan = (platform, hopperPath, clients) => [
18
- ...(hopperPath === undefined
19
- ? [
20
- {
21
- kind: "install_hopper",
22
- target: platform === "darwin"
23
- ? "~/Applications/Hopper Disassembler.app"
24
- : "system package manager",
25
- detail: "Download the official Hopper package, verify it, install it, and open Hopper for activation.",
26
- external: true,
27
- },
28
- ]
29
- : []),
30
- ...clients
31
- .filter(({ format }) => format !== "unsupported")
32
- .map((client) => ({
33
- kind: "configure_client",
34
- target: client.configPath,
35
- detail: `Add the REA MCP registration for ${client.name}; preserve unrelated configuration.`,
36
- external: false,
37
- })),
38
- {
39
- kind: "install_skill",
40
- target: "~/.agents/skills/rea-analysis/SKILL.md",
41
- detail: "Install or update the bundled REA analysis skill.",
42
- external: false,
43
- },
44
- ];
45
- const configureDetectedClients = async (options) => {
46
- for (const client of options.detectedClients) {
47
- const result = await options.host.configureClient(client, options.hopperPath, registrationCommand());
48
- options.clients[client.name] = result;
49
- if (result.status === "failed")
50
- return `${client.name} configuration ${result.reason} verification failed; no successful configuration was reported.`;
51
- if (result.status === "configured")
52
- options.appliedActions.push(`configured_${client.name}`);
53
- }
54
- return undefined;
55
- };
56
21
  /**
57
22
  * Install prerequisites and configure detected clients idempotently.
58
23
  * Discovery always precedes mutation. Interactive confirmation or explicit
@@ -74,8 +39,13 @@ export const runSetup = async (options, host = systemSetupHost(), confirm) => {
74
39
  if (unsupported !== undefined)
75
40
  return fail(unsupported);
76
41
  let hopperPath = await host.hopperPath();
42
+ const initialDoctor = await host.doctor();
43
+ const linuxHopperRepairNeeded = initialDoctor.checks.some(({ name, ok, detail }) => !ok &&
44
+ (name === "hopper-demo-runtime" ||
45
+ (name === "hopper-version" && detail === "/opt/hopper/bin/Hopper")));
46
+ const installHopper = hopperPath === undefined || linuxHopperRepairNeeded;
77
47
  const detectedClients = await host.detectedClients();
78
- plannedActions = setupPlan(host.platform, hopperPath, detectedClients);
48
+ plannedActions = setupPlan(host.platform, installHopper, detectedClients);
79
49
  let approved = options.approved;
80
50
  let interactiveApproval = false;
81
51
  if (!approved && confirm !== undefined && !options.structured) {
@@ -91,24 +61,26 @@ export const runSetup = async (options, host = systemSetupHost(), confirm) => {
91
61
  doctor: await host.doctor(),
92
62
  remediation: "Review the setup plan, then rerun interactively or with --yes.",
93
63
  };
94
- if (hopperPath === undefined &&
95
- (interactiveApproval || options.installHopper)) {
96
- hopperPath = await host.installHopper();
97
- if (hopperPath === undefined)
64
+ if (installHopper && (interactiveApproval || options.installHopper)) {
65
+ const installed = await host.installHopper();
66
+ if (installed.status === "failed")
98
67
  return {
99
68
  status: "needs_human",
100
69
  plannedActions,
101
70
  appliedActions,
102
71
  clients,
103
72
  doctor: await host.doctor(),
104
- remediation: "Hopper installation failed; install Hopper manually or rerun setup after resolving the reported system error.",
73
+ code: installed.code,
74
+ remediation: installed.remediation,
105
75
  };
76
+ hopperPath = installed.launcherPath;
106
77
  appliedActions.push("installed_hopper");
107
78
  }
108
79
  const clientFailure = await configureDetectedClients({
109
80
  host,
110
81
  detectedClients,
111
82
  hopperPath,
83
+ command: registrationCommand(),
112
84
  clients,
113
85
  appliedActions,
114
86
  });
@@ -116,29 +88,29 @@ export const runSetup = async (options, host = systemSetupHost(), confirm) => {
116
88
  return fail(clientFailure);
117
89
  const skill = await host.installSkill();
118
90
  if (skill === "failed")
119
- return fail("Agent skill installation or readback failed.");
91
+ return fail("REA analysis skill could not be installed or verified. Check permissions for `~/.agents/skills`, then rerun setup.");
120
92
  if (skill === "installed")
121
93
  appliedActions.push("installed_skill");
122
94
  const doctor = await host.doctor();
123
- const activationRequired = appliedActions.includes("installed_hopper");
124
- const ready = doctor.healthy && !activationRequired;
95
+ const remediation = finalSetupRemediation(host.platform, appliedActions.includes("installed_hopper"), doctor.healthy, hopperPath);
125
96
  return {
126
- status: ready ? "ready" : "needs_human",
97
+ status: remediation === undefined ? "ready" : "needs_human",
127
98
  plannedActions,
128
99
  appliedActions,
129
100
  clients,
130
101
  doctor,
131
- ...(ready
132
- ? {}
133
- : {
134
- remediation: activationRequired
135
- ? "Open Hopper, complete its one-time activation, then rerun rea doctor --json."
136
- : hopperPath === undefined
137
- ? "Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis."
138
- : "Run rea doctor and apply each reported remediation.",
139
- }),
102
+ ...(remediation === undefined ? {} : { remediation }),
140
103
  };
141
104
  };
105
+ const finalSetupRemediation = (platform, installedHopper, healthy, hopperPath) => {
106
+ if (platform === "darwin" && installedHopper)
107
+ return "Open Hopper, choose its demo mode or activate a license, then rerun rea doctor --json.";
108
+ if (healthy)
109
+ return undefined;
110
+ return hopperPath === undefined
111
+ ? "Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis."
112
+ : "Run rea doctor and apply each reported remediation.";
113
+ };
142
114
  const hostRemediation = async (host) => {
143
115
  if (host.platform !== "darwin" && host.platform !== "linux")
144
116
  return "REA supports Hopper on macOS and selected 64-bit Linux distributions.";
@@ -167,7 +139,9 @@ const systemSetupHost = () => {
167
139
  const result = process.platform === "linux"
168
140
  ? await installLinuxHopper()
169
141
  : await installMacHopper();
170
- return result.status === "installed" ? result.launcherPath : undefined;
142
+ if (result.status === "installed")
143
+ return result;
144
+ return setupInstallFailure(result.reason);
171
145
  },
172
146
  detectedClients: () => detectClients(homedir()),
173
147
  configureClient: (client, hopperPath, command) => client.format === "unsupported"
@@ -179,7 +153,7 @@ const systemSetupHost = () => {
179
153
  doctor: () => runDoctor(undefined, doctorHost),
180
154
  };
181
155
  };
182
- /** Detect supported coding agents from stable per-user installation markers. */
156
+ /** Detect supported agents from stable per-user installation markers. */
183
157
  export const detectClients = async (home) => {
184
158
  const detected = [];
185
159
  for (const candidate of supportedClients(home))
@@ -187,46 +161,6 @@ export const detectClients = async (home) => {
187
161
  detected.push(candidate);
188
162
  return detected;
189
163
  };
190
- /** Describe every client location that setup or uninstall may own. */
191
- export const supportedClients = (home) => [
192
- {
193
- name: "claude_code",
194
- configPath: join(home, ".claude.json"),
195
- markerPath: join(home, ".claude"),
196
- },
197
- {
198
- name: "claude_desktop",
199
- configPath: join(home, "Library/Application Support/Claude/claude_desktop_config.json"),
200
- markerPath: join(home, "Library/Application Support/Claude"),
201
- },
202
- {
203
- name: "codex",
204
- configPath: join(home, ".codex/config.toml"),
205
- markerPath: join(home, ".codex"),
206
- format: "toml",
207
- },
208
- {
209
- name: "cursor",
210
- configPath: join(home, ".cursor/mcp.json"),
211
- markerPath: join(home, ".cursor"),
212
- },
213
- {
214
- name: "gemini_cli",
215
- configPath: join(home, ".gemini/settings.json"),
216
- markerPath: join(home, ".gemini"),
217
- },
218
- {
219
- name: "windsurf",
220
- configPath: join(home, ".codeium/windsurf/mcp_config.json"),
221
- markerPath: join(home, ".codeium/windsurf"),
222
- },
223
- {
224
- name: "devin",
225
- configPath: join(home, ".devin"),
226
- markerPath: join(home, ".devin"),
227
- format: "unsupported",
228
- },
229
- ];
230
164
  /** Back up, atomically update, and semantically read back one JSON MCP configuration. */
231
165
  export const configureJsonClient = async (client, hopperPath, command = [
232
166
  "npx",
@@ -0,0 +1,19 @@
1
+ const failedConfigurationMessage = (reason) => {
2
+ if (reason === "backup")
3
+ return "Agent configuration could not be backed up, so no change was made. Check file permissions, then rerun setup.";
4
+ if (reason === "write")
5
+ return "Agent configuration could not be updated. Check file permissions, then rerun setup.";
6
+ return "Agent configuration could not be verified after writing. Repair the configuration file or restore its `.rea.backup`, then rerun setup.";
7
+ };
8
+ /** Configure each detected agent, stopping after the first failed transaction. */
9
+ export const configureDetectedClients = async (options) => {
10
+ for (const client of options.detectedClients) {
11
+ const result = await options.host.configureClient(client, options.hopperPath, options.command);
12
+ options.clients[client.name] = result;
13
+ if (result.status === "failed")
14
+ return failedConfigurationMessage(result.reason);
15
+ if (result.status === "configured")
16
+ options.appliedActions.push(`configured_${client.name}`);
17
+ }
18
+ return undefined;
19
+ };
@@ -0,0 +1,53 @@
1
+ /** Map provider installer failures to stable setup codes and safe recovery. */
2
+ export const setupInstallFailure = (reason) => {
3
+ switch (reason) {
4
+ case "unsupported_host":
5
+ return {
6
+ status: "failed",
7
+ code: "unsupported_host",
8
+ remediation: "Use a supported host, then rerun rea setup.",
9
+ };
10
+ case "integrity":
11
+ return {
12
+ status: "failed",
13
+ code: "integrity_mismatch",
14
+ remediation: "The Hopper download failed integrity verification. Retry setup; if it repeats, update REA before installing.",
15
+ };
16
+ case "authorization_or_package_manager":
17
+ return {
18
+ status: "failed",
19
+ code: "authorization_or_package_manager_failed",
20
+ remediation: "Allow the system package-manager operation or install the planned Hopper package and Linux runtime dependencies, then rerun rea setup.",
21
+ };
22
+ case "launcher_missing":
23
+ return {
24
+ status: "failed",
25
+ code: "launcher_missing",
26
+ remediation: "The package operation completed but Hopper is not runnable. Run rea doctor and repair the reported launcher issue.",
27
+ };
28
+ case "runtime_dependencies":
29
+ return {
30
+ status: "failed",
31
+ code: "runtime_dependency_unavailable",
32
+ remediation: "Hopper was installed but required shared libraries are missing. Rerun rea setup, then apply the hopper-demo-runtime remediation from rea doctor.",
33
+ };
34
+ case "unsupported_hopper_build":
35
+ return {
36
+ status: "failed",
37
+ code: "unsupported_hopper_build",
38
+ remediation: "The installed Hopper launcher does not match the build supported by this REA release. Unset HOPPER_LAUNCHER_PATH, reinstall through rea setup, or update REA.",
39
+ };
40
+ case "cancelled":
41
+ return {
42
+ status: "failed",
43
+ code: "setup_cancelled",
44
+ remediation: "Setup was cancelled before Hopper was installed. Rerun rea setup when ready.",
45
+ };
46
+ default:
47
+ return {
48
+ status: "failed",
49
+ code: "download_failed",
50
+ remediation: "REA could not download or unpack the official Hopper package. Check network and filesystem access, then retry setup.",
51
+ };
52
+ }
53
+ };
@@ -0,0 +1,31 @@
1
+ /** Build the complete setup mutation plan before approval. */
2
+ export const setupPlan = (platform, installHopper, clients) => [
3
+ ...(installHopper
4
+ ? [
5
+ {
6
+ kind: "install_hopper",
7
+ target: platform === "darwin"
8
+ ? "~/Applications/Hopper Disassembler.app"
9
+ : "system package manager",
10
+ detail: platform === "linux"
11
+ ? "Download, verify, and install Hopper plus its Xvfb demo-session dependencies. For the supported demo build, REA uses a private display and selects Hopper's offered demo mode for each analysis session."
12
+ : "Download the official Hopper package, verify it, and install it. Hopper may show its demo or license prompt when first opened.",
13
+ external: true,
14
+ },
15
+ ]
16
+ : []),
17
+ ...clients
18
+ .filter(({ format }) => format !== "unsupported")
19
+ .map((client) => ({
20
+ kind: "configure_client",
21
+ target: client.configPath,
22
+ detail: `Add the REA MCP registration for ${client.name}; preserve unrelated configuration.`,
23
+ external: false,
24
+ })),
25
+ {
26
+ kind: "install_skill",
27
+ target: "~/.agents/skills/rea-analysis/SKILL.md",
28
+ detail: "Install or update the bundled REA analysis skill.",
29
+ external: false,
30
+ },
31
+ ];
@@ -0,0 +1,41 @@
1
+ import { join } from "node:path";
2
+ /** Describe every client location that setup, doctor, or uninstall may inspect. */
3
+ export const supportedClients = (home) => [
4
+ {
5
+ name: "claude_code",
6
+ configPath: join(home, ".claude.json"),
7
+ markerPath: join(home, ".claude"),
8
+ },
9
+ {
10
+ name: "claude_desktop",
11
+ configPath: join(home, "Library/Application Support/Claude/claude_desktop_config.json"),
12
+ markerPath: join(home, "Library/Application Support/Claude"),
13
+ },
14
+ {
15
+ name: "codex",
16
+ configPath: join(home, ".codex/config.toml"),
17
+ markerPath: join(home, ".codex"),
18
+ format: "toml",
19
+ },
20
+ {
21
+ name: "cursor",
22
+ configPath: join(home, ".cursor/mcp.json"),
23
+ markerPath: join(home, ".cursor"),
24
+ },
25
+ {
26
+ name: "gemini_cli",
27
+ configPath: join(home, ".gemini/settings.json"),
28
+ markerPath: join(home, ".gemini"),
29
+ },
30
+ {
31
+ name: "windsurf",
32
+ configPath: join(home, ".codeium/windsurf/mcp_config.json"),
33
+ markerPath: join(home, ".codeium/windsurf"),
34
+ },
35
+ {
36
+ name: "devin",
37
+ configPath: join(home, ".devin"),
38
+ markerPath: join(home, ".devin"),
39
+ format: "unsupported",
40
+ },
41
+ ];