rea-agents 2.1.0 → 2.2.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 +29 -10
- package/dist/application/AndroidApplicationService.js +17 -0
- package/dist/application/AppleApplicationService.js +17 -0
- package/dist/application/ClientRegistrationStatus.js +11 -4
- package/dist/application/InventoryProjectionEvidence.js +41 -0
- package/dist/application/InvestigationProviders.js +12 -0
- package/dist/application/ProcessPairedExperiment.js +72 -0
- package/dist/application/Setup.js +3 -3
- package/dist/application/SetupClientConfiguration.js +10 -6
- package/dist/application/SetupPlan.js +3 -2
- package/dist/application/SetupSkill.js +44 -40
- package/dist/application/Uninstall.js +7 -1
- package/dist/application/Upgrade.js +16 -1
- package/dist/cli/setupCommands.js +8 -3
- package/dist/cliSetup.js +29 -12
- package/dist/contracts/electronToolContracts.js +51 -3
- package/dist/contracts/managedToolContracts.js +7 -2
- package/dist/contracts/sessionStatusContract.js +26 -0
- package/dist/contracts/toolContractTypes.js +12 -1
- package/dist/contracts/toolContracts.js +3 -10
- package/dist/contracts/toolOutputSchemaGroups.js +51 -9
- package/dist/contracts/toolOutputSchemaPrimitives.js +33 -32
- package/dist/domain/androidApplication.js +206 -0
- package/dist/domain/appleApplication.js +221 -0
- package/dist/domain/boundedCartesianProjection.js +15 -0
- package/dist/domain/processComparison.js +19 -0
- package/dist/domain/processPairedExperiment.js +128 -0
- package/dist/domain/replayMachine.js +316 -0
- package/dist/domain/replayMachineRuntime.js +215 -0
- package/dist/domain/runtimeIdentification.js +210 -0
- package/dist/evaluation/CodexAgentEval.js +132 -0
- package/dist/generatedPackageMetadata.js +2 -2
- package/dist/identity.js +2 -1
- package/dist/server/createServer.js +5 -2
- package/dist/server/javascriptApplicationResult.js +85 -0
- package/dist/server/registerElectronTools.js +19 -5
- package/dist/server/registerEvidenceResources.js +2 -0
- package/dist/server/registerJavaScriptApplicationGraphResource.js +69 -0
- package/dist/server/registerManagedTools.js +18 -1
- package/dist/server/registerSessionStatusTool.js +95 -25
- package/dist/server/toolResult.js +10 -4
- package/package.json +2 -1
- package/skills/reverse-engineer-anything/SKILL.md +68 -227
- package/skills/reverse-engineer-anything/references/controlled-replay.md +12 -0
- package/skills/reverse-engineer-anything/references/evidence-workflows.md +33 -0
- package/skills/reverse-engineer-anything/references/javascript-applications.md +25 -0
- package/skills/reverse-engineer-anything/references/native-and-artifacts.md +36 -0
- package/skills/reverse-engineer-anything/references/runtime-observation.md +24 -0
package/README.md
CHANGED
|
@@ -36,17 +36,18 @@ Reverse engineering normally makes the operator choose a tool, learn its API, mo
|
|
|
36
36
|
|
|
37
37
|
## Just ask your agent
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
Run setup once. Agent integration installs an aligned MCP registration and the
|
|
40
|
+
bundled routing skill together:
|
|
40
41
|
|
|
41
42
|
```bash
|
|
42
|
-
npx
|
|
43
|
+
npx rea-agents setup
|
|
43
44
|
```
|
|
44
45
|
|
|
45
46
|
Then ask:
|
|
46
47
|
|
|
47
48
|
```text
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
Understand how search works in the Notes app, show me the evidence, and build a
|
|
50
|
+
similar feature for my project.
|
|
50
51
|
```
|
|
51
52
|
|
|
52
53
|
Notes is only an example. Name any app you want to understand, or ask the agent to start with an overview.
|
|
@@ -97,9 +98,9 @@ shows its complete plan and asks before applying it. Setup does not update
|
|
|
97
98
|
Homebrew, Node.js, or npm. `npx rea-agents setup` opens with the work it
|
|
98
99
|
enables: investigate local apps from an agent, recover evidence through a
|
|
99
100
|
deep-analysis provider, and reuse REA's guided workflow. It summarizes the
|
|
100
|
-
detected agents, then asks which
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
detected agents, then asks which capabilities to set up: agent integration
|
|
102
|
+
(MCP plus the matching guided workflow) and—when needed—the Hopper provider.
|
|
103
|
+
Nothing is preselected. Choosing agent integration opens a second empty
|
|
103
104
|
checklist for the specific detected agents that should receive a registration.
|
|
104
105
|
|
|
105
106
|
REA keeps the journey inline so its history remains in the terminal. Selecting
|
|
@@ -131,10 +132,12 @@ Pass installer options after `bash -s --`, for example `--dry-run`, `--no-setup`
|
|
|
131
132
|
### With an agent — recommended
|
|
132
133
|
|
|
133
134
|
```bash
|
|
134
|
-
npx
|
|
135
|
+
npx rea-agents setup
|
|
135
136
|
```
|
|
136
137
|
|
|
137
|
-
|
|
138
|
+
Choose Agent Integration in the reviewed setup plan. REA installs the pinned MCP
|
|
139
|
+
registration and its matching routing skill as one transaction. After setup,
|
|
140
|
+
restart the configured agent so it loads the aligned integration.
|
|
138
141
|
|
|
139
142
|
Review the setup plan, approve it if appropriate, then describe the app or feature you want to understand. Hopper can run in its free demo mode; if it shows a first-run prompt, choose the demo or enter an existing license.
|
|
140
143
|
|
|
@@ -456,17 +459,28 @@ Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf,
|
|
|
456
459
|
|
|
457
460
|
### Manual MCP configuration
|
|
458
461
|
|
|
462
|
+
<!-- x-release-please-start-version -->
|
|
463
|
+
|
|
459
464
|
```json
|
|
460
465
|
{
|
|
461
466
|
"mcpServers": {
|
|
462
467
|
"rea": {
|
|
463
468
|
"command": "npx",
|
|
464
|
-
"args": ["-y", "rea-agents@
|
|
469
|
+
"args": ["-y", "rea-agents@2.2.0", "mcp"]
|
|
465
470
|
}
|
|
466
471
|
}
|
|
467
472
|
}
|
|
468
473
|
```
|
|
469
474
|
|
|
475
|
+
<!-- x-release-please-end -->
|
|
476
|
+
|
|
477
|
+
Persistent registrations should use one exact package version. `rea setup`
|
|
478
|
+
maintains that pin, upgrades the bundled skill at the same time, and gives Codex
|
|
479
|
+
a 30-second startup allowance for a cold package-runner start. An interactive
|
|
480
|
+
`rea upgrade` opens the updated setup plan after installing the new executable;
|
|
481
|
+
structured or non-interactive upgrades tell you to run that sync explicitly.
|
|
482
|
+
Restart clients whose approved registration changed.
|
|
483
|
+
|
|
470
484
|
MCP clients that support prompts can also discover six ordered investigation
|
|
471
485
|
workflows through `prompts/list`. Their optional identifier arguments use the
|
|
472
486
|
current session for bounded `completion/complete` suggestions; see
|
|
@@ -693,6 +707,11 @@ Any agent that can run a local MCP server can use the manual configuration. Setu
|
|
|
693
707
|
|
|
694
708
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, architecture, tests, and release instructions. Generated API documentation is available under [`docs/api`](docs/api/index.html).
|
|
695
709
|
|
|
710
|
+
`npm run verify:agent` runs brandless native, JavaScript-application, managed,
|
|
711
|
+
and browser prompts through a real local Codex CLI. Its JSON report measures
|
|
712
|
+
natural MCP use, first-tool routing, repeated calls, actual Codex token usage,
|
|
713
|
+
completion quality, and explicit treatment of authority and unknowns.
|
|
714
|
+
|
|
696
715
|
## Project links
|
|
697
716
|
|
|
698
717
|
[npm](https://www.npmjs.com/package/rea-agents) · [Issues](https://github.com/morluto/rea/issues) · [Security](SECURITY.md) · [Contributing](CONTRIBUTING.md) · [Hopper](https://www.hopperapp.com/) · [Ghidra](https://github.com/NationalSecurityAgency/ghidra)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { androidApplicationProjectionInputSchema, projectAndroidApplication, } from "../domain/androidApplication.js";
|
|
2
|
+
import { ANDROID_APPLICATION_PROVIDER } from "./InvestigationProviders.js";
|
|
3
|
+
import { projectInventoryEvidence } from "./InventoryProjectionEvidence.js";
|
|
4
|
+
const OPERATION = "project_android_application_graph";
|
|
5
|
+
/** Project authenticated APK inventory Evidence into Android application Evidence. */
|
|
6
|
+
export const projectAndroidApplicationEvidence = (rawInput) => {
|
|
7
|
+
return projectInventoryEvidence({
|
|
8
|
+
rawInput,
|
|
9
|
+
schema: androidApplicationProjectionInputSchema,
|
|
10
|
+
project: projectAndroidApplication,
|
|
11
|
+
operation: OPERATION,
|
|
12
|
+
predicateType: "rea.android-application-graph/v1",
|
|
13
|
+
provider: ANDROID_APPLICATION_PROVIDER,
|
|
14
|
+
subjectFormat: () => "apk",
|
|
15
|
+
protocolError: "Android application projection produced an invalid result",
|
|
16
|
+
});
|
|
17
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { appleApplicationProjectionInputSchema, projectAppleApplication, } from "../domain/appleApplication.js";
|
|
2
|
+
import { APPLE_APPLICATION_PROVIDER } from "./InvestigationProviders.js";
|
|
3
|
+
import { projectInventoryEvidence } from "./InventoryProjectionEvidence.js";
|
|
4
|
+
const OPERATION = "project_apple_application_graph";
|
|
5
|
+
/** Project authenticated IPA inventory Evidence into Apple application evidence. */
|
|
6
|
+
export const projectAppleApplicationEvidence = (rawInput) => {
|
|
7
|
+
return projectInventoryEvidence({
|
|
8
|
+
rawInput,
|
|
9
|
+
schema: appleApplicationProjectionInputSchema,
|
|
10
|
+
project: projectAppleApplication,
|
|
11
|
+
operation: OPERATION,
|
|
12
|
+
predicateType: "rea.apple-application-graph/v1",
|
|
13
|
+
provider: APPLE_APPLICATION_PROVIDER,
|
|
14
|
+
subjectFormat: () => "ipa",
|
|
15
|
+
protocolError: "Apple application projection produced an invalid result",
|
|
16
|
+
});
|
|
17
|
+
};
|
|
@@ -6,7 +6,11 @@ import { PRODUCT_IDENTITY } from "../identity.js";
|
|
|
6
6
|
import { supportedClients } from "./SupportedClients.js";
|
|
7
7
|
const objectSchema = z.record(z.string(), z.unknown());
|
|
8
8
|
const registrationSchema = z
|
|
9
|
-
.object({
|
|
9
|
+
.object({
|
|
10
|
+
command: z.string().min(1),
|
|
11
|
+
args: z.array(z.string()).default([]),
|
|
12
|
+
startup_timeout_sec: z.number().positive().optional(),
|
|
13
|
+
})
|
|
10
14
|
.passthrough();
|
|
11
15
|
/** Inspect supported client registrations without reading their environment. */
|
|
12
16
|
export const readClientRegistrationStatuses = async (home, currentCommandPath = resolve(process.argv[1] ?? "unknown")) => {
|
|
@@ -25,7 +29,7 @@ export const readClientRegistrationStatuses = async (home, currentCommandPath =
|
|
|
25
29
|
}
|
|
26
30
|
const registration = registrationSchema.parse(raw);
|
|
27
31
|
const command = [registration.command, ...registration.args];
|
|
28
|
-
statuses.push(status(client.name, client.configPath, command, registrationAligned(
|
|
32
|
+
statuses.push(status(client.name, client.configPath, command, registrationAligned(registration, client.name, currentCommandPath)
|
|
29
33
|
? "aligned"
|
|
30
34
|
: "stale"));
|
|
31
35
|
}
|
|
@@ -35,11 +39,14 @@ export const readClientRegistrationStatuses = async (home, currentCommandPath =
|
|
|
35
39
|
}
|
|
36
40
|
return statuses.sort((left, right) => left.client.localeCompare(right.client));
|
|
37
41
|
};
|
|
38
|
-
const registrationAligned = (
|
|
42
|
+
const registrationAligned = (registration, client, currentCommandPath) => {
|
|
43
|
+
const command = [registration.command, ...registration.args];
|
|
44
|
+
if (client === "codex" && registration.startup_timeout_sec !== 30)
|
|
45
|
+
return false;
|
|
39
46
|
if (command.length === 4 &&
|
|
40
47
|
command[0] === "npx" &&
|
|
41
48
|
command[1] === "-y" &&
|
|
42
|
-
command[2] === PRODUCT_IDENTITY.
|
|
49
|
+
command[2] === PRODUCT_IDENTITY.registrationPackageSpecifier &&
|
|
43
50
|
command[3] === "mcp")
|
|
44
51
|
return true;
|
|
45
52
|
return (command.length === 2 &&
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AnalysisInputError, AnalysisProtocolError, } from "../domain/errors.js";
|
|
3
|
+
import { createEvidence, } from "../domain/evidence.js";
|
|
4
|
+
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
5
|
+
import { err, ok } from "../domain/result.js";
|
|
6
|
+
/** Parse one inventory projection and wrap its deterministic result in Evidence v2. */
|
|
7
|
+
export const projectInventoryEvidence = (options) => {
|
|
8
|
+
const parsed = options.schema.safeParse(options.rawInput);
|
|
9
|
+
if (!parsed.success)
|
|
10
|
+
return err(new AnalysisInputError(options.operation, { cause: parsed.error }));
|
|
11
|
+
try {
|
|
12
|
+
const result = options.project(parsed.data);
|
|
13
|
+
const first = parsed.data.inventory_evidence[0];
|
|
14
|
+
return ok(createEvidence(first?.subject === null || first?.subject === undefined
|
|
15
|
+
? undefined
|
|
16
|
+
: {
|
|
17
|
+
path: first.subject.local_path,
|
|
18
|
+
sha256: result.root_sha256,
|
|
19
|
+
format: options.subjectFormat(first),
|
|
20
|
+
}, options.provider, {
|
|
21
|
+
predicateType: options.predicateType,
|
|
22
|
+
operation: options.operation,
|
|
23
|
+
parameters: {
|
|
24
|
+
inventory_evidence_ids: [...result.source_evidence_ids],
|
|
25
|
+
limits: jsonValueSchema.parse(parsed.data.limits),
|
|
26
|
+
},
|
|
27
|
+
result: jsonValueSchema.parse(result),
|
|
28
|
+
rawResult: null,
|
|
29
|
+
confidence: "inferred",
|
|
30
|
+
authority: "analyst-inference",
|
|
31
|
+
environment: null,
|
|
32
|
+
limitations: result.limitations,
|
|
33
|
+
evidenceLinks: result.source_evidence_ids,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
catch (cause) {
|
|
37
|
+
return err(cause instanceof TypeError || cause instanceof z.ZodError
|
|
38
|
+
? new AnalysisInputError(options.operation, { cause })
|
|
39
|
+
: new AnalysisProtocolError(options.protocolError, { cause }));
|
|
40
|
+
}
|
|
41
|
+
};
|
|
@@ -27,6 +27,18 @@ export const MANAGED_WORKFLOW_PROVIDER = {
|
|
|
27
27
|
name: "REA managed-code workflows",
|
|
28
28
|
version: "1",
|
|
29
29
|
};
|
|
30
|
+
/** Provider identity for deterministic Apple application projections. */
|
|
31
|
+
export const APPLE_APPLICATION_PROVIDER = {
|
|
32
|
+
id: "rea-apple-application",
|
|
33
|
+
name: "REA Apple application workflows",
|
|
34
|
+
version: "1",
|
|
35
|
+
};
|
|
36
|
+
/** Provider identity for deterministic Android application projections. */
|
|
37
|
+
export const ANDROID_APPLICATION_PROVIDER = {
|
|
38
|
+
id: "rea-android-application",
|
|
39
|
+
name: "REA Android application workflows",
|
|
40
|
+
version: "1",
|
|
41
|
+
};
|
|
30
42
|
/** Provider identity for deterministic static JavaScript application analysis. */
|
|
31
43
|
export const JAVASCRIPT_APPLICATION_PROVIDER = {
|
|
32
44
|
id: "rea-javascript-application",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { processCaptureCancelled, } from "./ProcessCaptureError.js";
|
|
2
|
+
import { captureProcessScenario } from "./ProcessHarness.js";
|
|
3
|
+
import { compareProcessCaptures } from "../domain/processComparison.js";
|
|
4
|
+
import { PROCESS_COMPARISON_DIMENSIONS } from "../domain/processComparison.js";
|
|
5
|
+
import { analyzeProcessRepeatability, bindProcessScenario, } from "../domain/processPairedExperiment.js";
|
|
6
|
+
import { err, ok } from "../domain/result.js";
|
|
7
|
+
const productionCapturePort = {
|
|
8
|
+
capture: captureProcessScenario,
|
|
9
|
+
};
|
|
10
|
+
const captureRepeats = async (input) => {
|
|
11
|
+
const captures = [];
|
|
12
|
+
for (let index = 0; index < input.repeatCount; index += 1) {
|
|
13
|
+
if (input.signal?.aborted === true)
|
|
14
|
+
return err(processCaptureCancelled());
|
|
15
|
+
const capture = await input.port.capture(input.scenario, input.policy, input.signal);
|
|
16
|
+
if (!capture.ok)
|
|
17
|
+
return err(capture.error);
|
|
18
|
+
captures.push(capture.value);
|
|
19
|
+
}
|
|
20
|
+
return ok(captures);
|
|
21
|
+
};
|
|
22
|
+
/** Execute repeatability-first authority/candidate captures under one contract. */
|
|
23
|
+
export const runPairedProcessExperiment = async (experiment, policy, options = {}) => {
|
|
24
|
+
const port = options.capturePort ?? productionCapturePort;
|
|
25
|
+
const authorityScenario = bindProcessScenario(experiment.shared_scenario, experiment.authority);
|
|
26
|
+
const candidateScenario = bindProcessScenario(experiment.shared_scenario, experiment.candidate);
|
|
27
|
+
const authorityRuns = await captureRepeats({
|
|
28
|
+
scenario: authorityScenario,
|
|
29
|
+
repeatCount: experiment.repeat_count,
|
|
30
|
+
policy,
|
|
31
|
+
port,
|
|
32
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
33
|
+
});
|
|
34
|
+
if (!authorityRuns.ok)
|
|
35
|
+
return authorityRuns;
|
|
36
|
+
const candidateRuns = await captureRepeats({
|
|
37
|
+
scenario: candidateScenario,
|
|
38
|
+
repeatCount: experiment.repeat_count,
|
|
39
|
+
policy,
|
|
40
|
+
port,
|
|
41
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
42
|
+
});
|
|
43
|
+
if (!candidateRuns.ok)
|
|
44
|
+
return candidateRuns;
|
|
45
|
+
const authorityRepeatability = analyzeProcessRepeatability(authorityRuns.value, experiment.required_dimensions);
|
|
46
|
+
const candidateRepeatability = analyzeProcessRepeatability(candidateRuns.value, experiment.required_dimensions);
|
|
47
|
+
const stable = authorityRepeatability.stable && candidateRepeatability.stable;
|
|
48
|
+
const allDimensionsRequired = PROCESS_COMPARISON_DIMENSIONS.every((dimension) => experiment.required_dimensions.includes(dimension));
|
|
49
|
+
const authorityLatest = authorityRuns.value.at(-1);
|
|
50
|
+
const candidateLatest = candidateRuns.value.at(-1);
|
|
51
|
+
const crossSide = stable &&
|
|
52
|
+
allDimensionsRequired &&
|
|
53
|
+
authorityLatest !== undefined &&
|
|
54
|
+
candidateLatest !== undefined
|
|
55
|
+
? compareProcessCaptures(authorityLatest, candidateLatest, {
|
|
56
|
+
maxCaptureAgeMs: experiment.freshness_policy.max_capture_age_ms,
|
|
57
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
58
|
+
})
|
|
59
|
+
: null;
|
|
60
|
+
return ok({
|
|
61
|
+
authority_runs: authorityRuns.value,
|
|
62
|
+
candidate_runs: candidateRuns.value,
|
|
63
|
+
authority_repeatability: authorityRepeatability,
|
|
64
|
+
candidate_repeatability: candidateRepeatability,
|
|
65
|
+
cross_side: crossSide,
|
|
66
|
+
cross_side_blocked_reason: crossSide !== null
|
|
67
|
+
? null
|
|
68
|
+
: !allDimensionsRequired
|
|
69
|
+
? "Cross-side comparison is blocked until every returned dimension is required and repeatable."
|
|
70
|
+
: "Cross-side comparison is blocked because a required same-side dimension is unstable.",
|
|
71
|
+
});
|
|
72
|
+
};
|
|
@@ -15,7 +15,7 @@ export { configureJsonClient, configureTomlClient };
|
|
|
15
15
|
import { setupInstallFailure, } from "./SetupInstallFailure.js";
|
|
16
16
|
export { canonicalSkillNeedsInstall, installCanonicalSkill };
|
|
17
17
|
const registrationCommand = () => process.env.npm_command === "exec"
|
|
18
|
-
?
|
|
18
|
+
? PRODUCT_IDENTITY.mcpCommand.split(" ")
|
|
19
19
|
: [resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), "mcp"];
|
|
20
20
|
/** Discover, approve, and apply setup actions idempotently. */
|
|
21
21
|
export const runSetup = async (options, host = systemSetupHost(), confirm) => {
|
|
@@ -47,7 +47,7 @@ export const runSetup = async (options, host = systemSetupHost(), confirm) => {
|
|
|
47
47
|
plannedActions,
|
|
48
48
|
appliedActions,
|
|
49
49
|
clients,
|
|
50
|
-
doctor:
|
|
50
|
+
doctor: discovery.initialDoctor,
|
|
51
51
|
remediation: "Review the setup plan, then rerun interactively or with --yes.",
|
|
52
52
|
};
|
|
53
53
|
let detectedClients = selection.detectedClients;
|
|
@@ -110,7 +110,7 @@ const resolveSetupSelection = async (options, discovery, confirm) => {
|
|
|
110
110
|
installSkill = plannedActions.some(({ kind }) => kind === "install_skill");
|
|
111
111
|
detectedClients = detectedClients.filter((client) => selected.has(`configure_client:${client.name}`));
|
|
112
112
|
interactiveApproval = decision.approved;
|
|
113
|
-
approved = decision.approved
|
|
113
|
+
approved = decision.approved;
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
return {
|
|
@@ -9,9 +9,10 @@ import { resolveClientConfigTransactionPath } from "./ClientConfigPath.js";
|
|
|
9
9
|
const defaultCommand = () => [
|
|
10
10
|
"npx",
|
|
11
11
|
"-y",
|
|
12
|
-
PRODUCT_IDENTITY.
|
|
12
|
+
PRODUCT_IDENTITY.registrationPackageSpecifier,
|
|
13
13
|
"mcp",
|
|
14
14
|
];
|
|
15
|
+
const CODEX_STARTUP_TIMEOUT_SECONDS = 30;
|
|
15
16
|
/** Back up, atomically update, and semantically read back one JSON MCP configuration. */
|
|
16
17
|
export const configureJsonClient = async (client, environment = {}, command = defaultCommand()) => {
|
|
17
18
|
const transactionPath = await resolveClientConfigTransactionPath(client.configPath);
|
|
@@ -34,7 +35,7 @@ export const configureJsonClient = async (client, environment = {}, command = de
|
|
|
34
35
|
catch {
|
|
35
36
|
return { status: "failed", reason: "readback" };
|
|
36
37
|
}
|
|
37
|
-
const desired = clientConfigurationDesired(normalizeProviderEnvironment(environment), command);
|
|
38
|
+
const desired = clientConfigurationDesired(client, normalizeProviderEnvironment(environment), command);
|
|
38
39
|
if (sameConfiguration(servers[PRODUCT_IDENTITY.mcpServerKey], desired))
|
|
39
40
|
return { status: "unchanged" };
|
|
40
41
|
let backupPath;
|
|
@@ -95,7 +96,7 @@ export const configureTomlClient = async (client, environment = {}, command = de
|
|
|
95
96
|
catch {
|
|
96
97
|
return { status: "failed", reason: "readback" };
|
|
97
98
|
}
|
|
98
|
-
const desired = clientConfigurationDesired(normalizeProviderEnvironment(environment), command);
|
|
99
|
+
const desired = clientConfigurationDesired(client, normalizeProviderEnvironment(environment), command);
|
|
99
100
|
if (sameConfiguration(servers[PRODUCT_IDENTITY.mcpServerKey], desired))
|
|
100
101
|
return { status: "unchanged" };
|
|
101
102
|
const backupPath = original === undefined ? undefined : `${client.configPath}.rea.backup`;
|
|
@@ -127,7 +128,7 @@ export const configureTomlClient = async (client, environment = {}, command = de
|
|
|
127
128
|
};
|
|
128
129
|
/** Determine whether an existing client configuration matches the desired registration. */
|
|
129
130
|
export const clientConfigurationAligned = async (client, providerEnvironment, command) => {
|
|
130
|
-
const desired = clientConfigurationDesired(providerEnvironment, command);
|
|
131
|
+
const desired = clientConfigurationDesired(client, providerEnvironment, command);
|
|
131
132
|
try {
|
|
132
133
|
const original = await readFile(client.configPath, "utf8");
|
|
133
134
|
const document = client.format === "toml"
|
|
@@ -162,7 +163,7 @@ export const inspectClientConfiguration = async (client, providerEnvironment, co
|
|
|
162
163
|
remediation: "The configuration file could not be read. Check its permissions before rerunning setup.",
|
|
163
164
|
};
|
|
164
165
|
}
|
|
165
|
-
const desired = clientConfigurationDesired(providerEnvironment, command);
|
|
166
|
+
const desired = clientConfigurationDesired(client, providerEnvironment, command);
|
|
166
167
|
try {
|
|
167
168
|
const document = client.format === "toml"
|
|
168
169
|
? objectSchema.parse(parseToml(original))
|
|
@@ -207,11 +208,14 @@ const restoreConfig = async (path, original) => {
|
|
|
207
208
|
// The backup remains available for the remediation reported by setup.
|
|
208
209
|
}
|
|
209
210
|
};
|
|
210
|
-
const clientConfigurationDesired = (providerEnvironment, command) => {
|
|
211
|
+
const clientConfigurationDesired = (client, providerEnvironment, command) => {
|
|
211
212
|
const environment = Object.fromEntries(Object.entries(providerEnvironment).sort(([left], [right]) => left.localeCompare(right)));
|
|
212
213
|
return {
|
|
213
214
|
command: command[0] ?? PRODUCT_IDENTITY.cliBinary,
|
|
214
215
|
args: command.slice(1),
|
|
216
|
+
...(client.name === "codex"
|
|
217
|
+
? { startup_timeout_sec: CODEX_STARTUP_TIMEOUT_SECONDS }
|
|
218
|
+
: {}),
|
|
215
219
|
...(Object.keys(environment).length === 0 ? {} : { env: environment }),
|
|
216
220
|
};
|
|
217
221
|
};
|
|
@@ -48,8 +48,8 @@ const setupPlan = (input) => [
|
|
|
48
48
|
id: "install_skill",
|
|
49
49
|
kind: "install_skill",
|
|
50
50
|
label: "REA reverse-engineering skill",
|
|
51
|
-
target: join(homedir(), ".agents/skills", PRODUCT_IDENTITY.skillName
|
|
52
|
-
detail: "Install or update the bundled REA reverse-engineering skill.",
|
|
51
|
+
target: join(homedir(), ".agents/skills", PRODUCT_IDENTITY.skillName),
|
|
52
|
+
detail: "Install or update the bundled REA reverse-engineering skill and on-demand references.",
|
|
53
53
|
external: false,
|
|
54
54
|
operation: "install",
|
|
55
55
|
},
|
|
@@ -112,6 +112,7 @@ export const discoverSetupActions = async (input) => {
|
|
|
112
112
|
: []);
|
|
113
113
|
const linuxDistribution = host.platform === "linux" ? await host.linuxDistribution() : undefined;
|
|
114
114
|
return {
|
|
115
|
+
initialDoctor,
|
|
115
116
|
installHopper,
|
|
116
117
|
installSkill,
|
|
117
118
|
detectedClients: clientPlans.map(({ client }) => client),
|
|
@@ -2,7 +2,15 @@ import { mkdir, readFile, rm } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import writeFileAtomic from "write-file-atomic";
|
|
4
4
|
import { PRODUCT_IDENTITY } from "../identity.js";
|
|
5
|
-
const
|
|
5
|
+
const SKILL_FILES = [
|
|
6
|
+
"SKILL.md",
|
|
7
|
+
"references/native-and-artifacts.md",
|
|
8
|
+
"references/javascript-applications.md",
|
|
9
|
+
"references/runtime-observation.md",
|
|
10
|
+
"references/evidence-workflows.md",
|
|
11
|
+
"references/controlled-replay.md",
|
|
12
|
+
];
|
|
13
|
+
const skillRoot = (home) => join(home, ".agents/skills", PRODUCT_IDENTITY.skillName);
|
|
6
14
|
const readOptionalText = async (path) => {
|
|
7
15
|
try {
|
|
8
16
|
return await readFile(path, "utf8");
|
|
@@ -13,62 +21,58 @@ const readOptionalText = async (path) => {
|
|
|
13
21
|
throw cause;
|
|
14
22
|
}
|
|
15
23
|
};
|
|
16
|
-
const canonicalSkillFiles = async (home) => ({
|
|
17
|
-
destination
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
const canonicalSkillFiles = async (home) => Promise.all(SKILL_FILES.map(async (relativePath) => {
|
|
25
|
+
const destination = join(skillRoot(home), relativePath);
|
|
26
|
+
return {
|
|
27
|
+
destination,
|
|
28
|
+
content: await readFile(new URL(`../../skills/${PRODUCT_IDENTITY.skillName}/${relativePath}`, import.meta.url), "utf8"),
|
|
29
|
+
original: await readOptionalText(destination),
|
|
30
|
+
};
|
|
31
|
+
}));
|
|
32
|
+
/** Report whether setup would change any file in the managed REA skill bundle. */
|
|
21
33
|
export const canonicalSkillNeedsInstall = async (home) => {
|
|
22
34
|
try {
|
|
23
|
-
|
|
24
|
-
return (await readOptionalText(destination)) !== content;
|
|
35
|
+
return (await canonicalSkillFiles(home)).some(({ content, original }) => original !== content);
|
|
25
36
|
}
|
|
26
37
|
catch {
|
|
27
38
|
return true;
|
|
28
39
|
}
|
|
29
40
|
};
|
|
30
|
-
|
|
41
|
+
const writeText = (path, content) => writeFileAtomic(path, content, { encoding: "utf8", mode: 0o600 });
|
|
42
|
+
const restoreSkillFiles = async (changed) => {
|
|
43
|
+
for (const { destination, original } of [...changed].reverse()) {
|
|
44
|
+
if (original === undefined)
|
|
45
|
+
await rm(destination, { force: true });
|
|
46
|
+
else
|
|
47
|
+
await writeText(destination, original);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/** Transactionally install or upgrade the canonical REA skill and references. */
|
|
31
51
|
export const installCanonicalSkill = async (home) => {
|
|
32
|
-
let
|
|
33
|
-
let backup = `${destination}.rea.backup`;
|
|
34
|
-
let original;
|
|
35
|
-
let canonicalChanged = false;
|
|
52
|
+
let changed = [];
|
|
36
53
|
try {
|
|
37
54
|
const canonical = await canonicalSkillFiles(home);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const { content } = canonical;
|
|
41
|
-
original = await readOptionalText(destination);
|
|
42
|
-
if (original === content)
|
|
55
|
+
changed = canonical.filter(({ content, original }) => original !== content);
|
|
56
|
+
if (changed.length === 0)
|
|
43
57
|
return "unchanged";
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
await
|
|
53
|
-
|
|
54
|
-
mode: 0o600,
|
|
55
|
-
});
|
|
56
|
-
if ((await readFile(destination, "utf8")) !== content)
|
|
57
|
-
throw new Error("skill readback mismatch");
|
|
58
|
+
for (const { destination, original } of changed) {
|
|
59
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
60
|
+
if (original !== undefined)
|
|
61
|
+
await writeText(`${destination}.rea.backup`, original);
|
|
62
|
+
}
|
|
63
|
+
for (const { destination, content } of changed)
|
|
64
|
+
await writeText(destination, content);
|
|
65
|
+
for (const { destination, content } of changed)
|
|
66
|
+
if ((await readFile(destination, "utf8")) !== content)
|
|
67
|
+
throw new Error(`skill readback mismatch: ${destination}`);
|
|
58
68
|
return "installed";
|
|
59
69
|
}
|
|
60
70
|
catch {
|
|
61
71
|
try {
|
|
62
|
-
|
|
63
|
-
await rm(destination, { force: true });
|
|
64
|
-
else if (canonicalChanged && original !== undefined)
|
|
65
|
-
await writeFileAtomic(destination, original, {
|
|
66
|
-
encoding: "utf8",
|
|
67
|
-
mode: 0o600,
|
|
68
|
-
});
|
|
72
|
+
await restoreSkillFiles(changed);
|
|
69
73
|
}
|
|
70
74
|
catch {
|
|
71
|
-
//
|
|
75
|
+
// Per-file backups remain beside changed files for operator recovery.
|
|
72
76
|
}
|
|
73
77
|
return "failed";
|
|
74
78
|
}
|
|
@@ -128,7 +128,13 @@ const isOwnedRegistration = (value) => {
|
|
|
128
128
|
args[0] === "mcp") ||
|
|
129
129
|
(command === "npx" &&
|
|
130
130
|
(JSON.stringify(args) ===
|
|
131
|
-
JSON.stringify([
|
|
131
|
+
JSON.stringify([
|
|
132
|
+
"-y",
|
|
133
|
+
PRODUCT_IDENTITY.registrationPackageSpecifier,
|
|
134
|
+
"mcp",
|
|
135
|
+
]) ||
|
|
136
|
+
JSON.stringify(args) ===
|
|
137
|
+
JSON.stringify(["-y", PRODUCT_IDENTITY.packageSpecifier, "mcp"]) ||
|
|
132
138
|
JSON.stringify(args) ===
|
|
133
139
|
JSON.stringify(["-y", PRODUCT_IDENTITY.packageName, "mcp"]))));
|
|
134
140
|
};
|
|
@@ -9,6 +9,7 @@ import { PRODUCT_IDENTITY } from "../identity.js";
|
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
const registryResponseSchema = z.object({ version: z.string().min(1) });
|
|
11
11
|
const UPGRADE_COMMAND = "npm install --global rea-agents@latest";
|
|
12
|
+
const CLI_SCRIPT_PATH = fileURLToPath(new URL("../../scripts/rea.mjs", import.meta.url));
|
|
12
13
|
/** Check the npm registry and update the same global REA installation. */
|
|
13
14
|
export const runUpgrade = async (currentVersion, host = systemUpgradeHost(), output = "human") => {
|
|
14
15
|
const latestVersion = await host.latestVersion();
|
|
@@ -46,6 +47,12 @@ export const runUpgrade = async (currentVersion, host = systemUpgradeHost(), out
|
|
|
46
47
|
reason: "install",
|
|
47
48
|
remediation: `REA could not update through npm. Check npm registry access and global install permissions, then run: ${UPGRADE_COMMAND}`,
|
|
48
49
|
};
|
|
50
|
+
const integrationSyncResult = await host.syncAgentIntegration(output);
|
|
51
|
+
const integrationSync = integrationSyncResult === undefined
|
|
52
|
+
? "deferred"
|
|
53
|
+
: integrationSyncResult
|
|
54
|
+
? "setup_invoked"
|
|
55
|
+
: "failed";
|
|
49
56
|
return {
|
|
50
57
|
status: "upgraded",
|
|
51
58
|
previousVersion: currentVersion,
|
|
@@ -53,8 +60,11 @@ export const runUpgrade = async (currentVersion, host = systemUpgradeHost(), out
|
|
|
53
60
|
versionCheck: "available",
|
|
54
61
|
installMethod: "npm",
|
|
55
62
|
command: UPGRADE_COMMAND,
|
|
63
|
+
integrationSync,
|
|
56
64
|
clientRestartRequired: true,
|
|
57
|
-
remediation:
|
|
65
|
+
remediation: integrationSync === "setup_invoked"
|
|
66
|
+
? "The updated setup journey was opened to align registrations and the skill. Restart clients whose approved registration changed."
|
|
67
|
+
: "Run rea setup --all-detected to align registrations and the skill, then restart clients that may retain an older MCP server.",
|
|
58
68
|
};
|
|
59
69
|
};
|
|
60
70
|
/** Create the npm registry and subprocess effects for a production upgrade. */
|
|
@@ -79,6 +89,11 @@ export const systemUpgradeHost = () => ({
|
|
|
79
89
|
prefix,
|
|
80
90
|
`${PRODUCT_IDENTITY.packageName}@latest`,
|
|
81
91
|
], output),
|
|
92
|
+
syncAgentIntegration: (output) => output === "structured" ||
|
|
93
|
+
process.stdin.isTTY !== true ||
|
|
94
|
+
process.stderr.isTTY !== true
|
|
95
|
+
? Promise.resolve(undefined)
|
|
96
|
+
: runCommand(process.execPath, [CLI_SCRIPT_PATH, "setup", "--all-detected"], "human"),
|
|
82
97
|
});
|
|
83
98
|
const systemNpmInstallationHost = {
|
|
84
99
|
canonicalPath: realpath,
|
|
@@ -39,7 +39,7 @@ export const registerSetupCommands = (cli, logger) => {
|
|
|
39
39
|
skill: z
|
|
40
40
|
.boolean()
|
|
41
41
|
.optional()
|
|
42
|
-
.describe("
|
|
42
|
+
.describe("Override the bundled skill included with agent integrations"),
|
|
43
43
|
dryRun: z
|
|
44
44
|
.boolean()
|
|
45
45
|
.default(false)
|
|
@@ -84,7 +84,8 @@ const runSetupCommand = async (input) => {
|
|
|
84
84
|
process.stderr.isTTY === true;
|
|
85
85
|
const hasExplicitScope = setupHasExplicitScope(options);
|
|
86
86
|
if (options.yes && !hasExplicitScope)
|
|
87
|
-
process.stderr.write("! Implicit `rea setup --yes` scope is deprecated; use `--all-detected
|
|
87
|
+
process.stderr.write("! Implicit `rea setup --yes` scope is deprecated; use `--all-detected` to retain it.\n");
|
|
88
|
+
const agentIntegrationSelected = options.allDetected || options.client.length > 0;
|
|
88
89
|
const result = await runSetup({
|
|
89
90
|
approved: options.yes && !options.dryRun,
|
|
90
91
|
installHopper: options.installHopper,
|
|
@@ -93,7 +94,11 @@ const runSetupCommand = async (input) => {
|
|
|
93
94
|
...(!hasExplicitScope || options.allDetected
|
|
94
95
|
? {}
|
|
95
96
|
: { clientIds: options.client }),
|
|
96
|
-
...(!hasExplicitScope
|
|
97
|
+
...(!hasExplicitScope
|
|
98
|
+
? {}
|
|
99
|
+
: {
|
|
100
|
+
installSkill: options.skill ?? agentIntegrationSelected,
|
|
101
|
+
}),
|
|
97
102
|
...(interactive ? { onProgress: renderSetupProgress } : {}),
|
|
98
103
|
}, systemSetupHost(createSystemDoctorHost()), interactive
|
|
99
104
|
? (actions) => confirmInteractiveSetup(actions, options.accessible)
|