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.
- package/README.md +95 -27
- package/bridge/hopper_bridge.py +172 -14
- package/dist/application/AnalysisSnapshotCache.js +131 -0
- package/dist/application/AnalysisSnapshotFiles.js +31 -0
- package/dist/application/ArtifactGraphConstruction.js +1 -1
- package/dist/application/ArtifactInventory.js +100 -24
- package/dist/application/AuthorizedArtifactInventory.js +16 -0
- package/dist/application/BinarySession.js +149 -36
- package/dist/application/BinarySessionPort.js +1 -0
- package/dist/application/BoundedJsonFiles.js +109 -0
- package/dist/application/CapabilityInventory.js +127 -0
- package/dist/application/ClientRegistrationStatus.js +69 -0
- package/dist/application/CrossVersionInventory.js +109 -0
- package/dist/application/CrossVersionInvestigation.js +358 -0
- package/dist/application/DirectAnalysis.js +205 -24
- package/dist/application/Doctor.js +104 -3
- package/dist/application/EvidenceBundleFiles.js +15 -108
- package/dist/application/EvidenceLedger.js +3 -2
- package/dist/application/InvestigationProviders.js +48 -0
- package/dist/application/InvestigationWorkspaceStore.js +214 -0
- package/dist/application/LinuxHopper.js +72 -48
- package/dist/application/PermissionAuthority.js +183 -0
- package/dist/application/PermissionConfiguration.js +26 -0
- package/dist/application/ProcessCaptureAuthority.js +2 -2
- package/dist/application/ProcessCaptureError.js +25 -0
- package/dist/application/ProcessCaptureLifecycle.js +22 -2
- package/dist/application/ProcessCli.js +111 -31
- package/dist/application/ProcessHarness.js +6 -3
- package/dist/application/ProgressReporter.js +30 -0
- package/dist/application/ProjectPermissionStore.js +110 -0
- package/dist/application/ReferenceSourceImportEntries.js +18 -3
- package/dist/application/ReferenceSourceImportTypes.js +32 -0
- package/dist/application/Setup.js +35 -101
- package/dist/application/SetupClients.js +19 -0
- package/dist/application/SetupInstallFailure.js +53 -0
- package/dist/application/SetupPlan.js +31 -0
- package/dist/application/SupportedClients.js +41 -0
- package/dist/application/Uninstall.js +34 -24
- package/dist/application/UnknownEvidence.js +33 -0
- package/dist/application/Upgrade.js +3 -1
- package/dist/application/runtime.js +2 -2
- package/dist/artifacts/ArtifactProvider.js +15 -8
- package/dist/catalogIdentity.js +121 -0
- package/dist/cli.js +76 -13
- package/dist/cliEvidenceCommands.js +52 -12
- package/dist/cliInvestigationCommands.js +137 -0
- package/dist/cliLogging.js +20 -3
- package/dist/cliOutput.js +41 -0
- package/dist/cliPolicyCommands.js +115 -0
- package/dist/config.js +102 -27
- package/dist/contracts/artifactToolContracts.js +14 -1
- package/dist/contracts/errorSchemas.js +98 -0
- package/dist/contracts/promptContracts.js +256 -0
- package/dist/contracts/sessionLifecycleInputs.js +11 -0
- package/dist/contracts/toolContractTypes.js +1 -0
- package/dist/contracts/toolContracts.js +20 -7
- package/dist/contracts/toolOutputSchemas.js +70 -9
- package/dist/domain/analysisSnapshot.js +149 -0
- package/dist/domain/artifactComparison.js +38 -11
- package/dist/domain/artifactGraph.js +25 -1
- package/dist/domain/artifactInventoryEvidence.js +18 -0
- package/dist/domain/changedBehavior.js +35 -10
- package/dist/domain/errors.js +384 -32
- package/dist/domain/evidence.js +4 -2
- package/dist/domain/evidenceBundle.js +28 -0
- package/dist/domain/hopperStartupFailure.js +55 -0
- package/dist/domain/investigationWorkspace.js +341 -0
- package/dist/domain/jsonValue.js +2 -0
- package/dist/domain/nativeInspection.js +3 -2
- package/dist/domain/permissionPolicy.js +157 -0
- package/dist/domain/processCapture.js +5 -4
- package/dist/domain/processComparison.js +6 -4
- package/dist/domain/reconstructionVerification.js +1 -1
- package/dist/domain/reconstructionVerificationSchemas.js +1 -0
- package/dist/domain/staticRuntimeCorrelation.js +5 -1
- package/dist/generatedPackageMetadata.js +9 -0
- package/dist/hopper/BridgeLauncher.js +51 -20
- package/dist/hopper/HopperClient.js +45 -3
- package/dist/hopper/HopperProvider.js +1 -0
- package/dist/identity.js +10 -3
- package/dist/logger.js +2 -2
- package/dist/main.js +139 -28
- package/dist/server/createServer.js +54 -5
- package/dist/server/mcpProgress.js +23 -0
- package/dist/server/promptCompletion.js +144 -0
- package/dist/server/registerArtifactComparisonTool.js +6 -2
- package/dist/server/registerBundleComparisonTool.js +6 -2
- package/dist/server/registerEnhancedTools.js +20 -7
- package/dist/server/registerEvidenceResources.js +302 -0
- package/dist/server/registerEvidenceTools.js +59 -8
- package/dist/server/registerFunctionComparisonTool.js +7 -3
- package/dist/server/registerInvestigationTools.js +111 -18
- package/dist/server/registerOfficialTools.js +25 -14
- package/dist/server/registerProcessComparisonTool.js +8 -10
- package/dist/server/registerPrompts.js +67 -0
- package/dist/server/registerSessionStatusTool.js +52 -0
- package/dist/server/registerSessionTools.js +178 -22
- package/dist/server/runDerivedOperation.js +35 -0
- package/dist/server/sessionEvidence.js +2 -8
- package/dist/server/sessionToolPolicies.js +1 -42
- package/dist/server/toolResult.js +34 -6
- package/dist/serverIdentity.js +70 -0
- package/install.sh +11 -11
- package/package.json +10 -6
- package/scripts/hopper-demo-x11.py +349 -0
- package/scripts/rea.mjs +6 -3
- 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, {
|
|
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
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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,
|
|
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,
|
|
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 (
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
|
124
|
-
const ready = doctor.healthy && !activationRequired;
|
|
95
|
+
const remediation = finalSetupRemediation(host.platform, appliedActions.includes("installed_hopper"), doctor.healthy, hopperPath);
|
|
125
96
|
return {
|
|
126
|
-
status:
|
|
97
|
+
status: remediation === undefined ? "ready" : "needs_human",
|
|
127
98
|
plannedActions,
|
|
128
99
|
appliedActions,
|
|
129
100
|
clients,
|
|
130
101
|
doctor,
|
|
131
|
-
...(
|
|
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
|
-
|
|
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
|
|
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
|
+
];
|